strongly typed AnkiConnect
This commit is contained in:
1 parent
9516f287c8
commit
a144c7fd74
3 files changed
+265
-57
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { AnkiConnect } from '@/ankiconnect';
|
||||
|
||||
async function main() {
|
||||
const anki = new AnkiConnect();
|
||||
console.log(
|
||||
await anki.multi<['findNotes', 'getTags']>({
|
||||
actions: [
|
||||
{
|
||||
action: 'findNotes',
|
||||
params: {
|
||||
query: 'added:1',
|
||||
},
|
||||
},
|
||||
{ action: 'getTags' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
await anki.api('findNotes', {
|
||||
query: 'deck:Takoboto',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Query syntax is documented here.
|
||||
*
|
||||
* @link https://docs.ankiweb.net/searching.html
|
||||
*/
|
||||
export type IAnkiQuery = string;
|
||||
|
||||
export interface IAnkiCard {
|
||||
sortf: number;
|
||||
did: number;
|
||||
|
||||
+231
-57
@@ -1,11 +1,6 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { IAnkiCard } from './anki';
|
||||
|
||||
export interface IAnkiConnectResponse<K extends keyof IAnkiConnectActions> {
|
||||
result: IAnkiConnectActions[K];
|
||||
error: string | null;
|
||||
}
|
||||
import { IAnkiCard, IAnkiQuery } from './anki';
|
||||
|
||||
export interface IAnkiConnectActions
|
||||
extends Record<
|
||||
@@ -37,31 +32,7 @@ export interface IAnkiConnectActions
|
||||
|
||||
// Media Actions
|
||||
storeMediaFile: {
|
||||
params: (
|
||||
| {
|
||||
/**
|
||||
* Specified base64-encoded contents
|
||||
*/
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
}
|
||||
) & {
|
||||
/**
|
||||
* To prevent Anki from removing files not used by any cards (e.g. for configuration files),
|
||||
* prefix the filename with an underscore.
|
||||
*/
|
||||
filename: string;
|
||||
/**
|
||||
* Any existing file with the same name is deleted by default.
|
||||
* Set `deleteExisting` to `false` to prevent that by letting Anki give the new file a non-conflicting name.
|
||||
*/
|
||||
deleteExisting?: boolean;
|
||||
};
|
||||
params: IAnkiConnectMedia;
|
||||
result: string;
|
||||
};
|
||||
retrieveMediaFile: {
|
||||
@@ -222,8 +193,112 @@ export interface IAnkiConnectActions
|
||||
|
||||
// Note Actions
|
||||
addNote: {
|
||||
params: {};
|
||||
result: number;
|
||||
params: {
|
||||
note: IAnkiConnectNote;
|
||||
};
|
||||
result: number | null;
|
||||
};
|
||||
addNotes: {
|
||||
params: {
|
||||
notes: IAnkiConnectNote[];
|
||||
};
|
||||
result: (number | null)[];
|
||||
};
|
||||
canAddNotes: {
|
||||
params: {
|
||||
notes: IAnkiConnectNote[];
|
||||
};
|
||||
result: boolean[];
|
||||
};
|
||||
updateNoteFields: {
|
||||
params: {
|
||||
note: {
|
||||
id: number;
|
||||
fields: {
|
||||
[fieldName: string]: string;
|
||||
};
|
||||
audio?: IAnkiConnectMediaInNote[];
|
||||
video?: IAnkiConnectMediaInNote[];
|
||||
picture?: IAnkiConnectMediaInNote[];
|
||||
};
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
addTags: {
|
||||
params: {
|
||||
notes: number[];
|
||||
/**
|
||||
* The docs don't make it clear if tags are space-separated.
|
||||
*
|
||||
* @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
|
||||
*/
|
||||
tags: string;
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
removeTags: {
|
||||
params: {
|
||||
notes: number[];
|
||||
/**
|
||||
* The docs don't make it clear if tags are space-separated.
|
||||
*
|
||||
* @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
|
||||
*/
|
||||
tags: string;
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
getTags: {
|
||||
result: string[];
|
||||
};
|
||||
cleanUnusedTags: {
|
||||
result: null;
|
||||
};
|
||||
replaceTags: {
|
||||
params: {
|
||||
notes: number[];
|
||||
tag_to_replace: string;
|
||||
replace_with_tag: string;
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
replaceTagsInAllNotes: {
|
||||
params: {
|
||||
tag_to_replace: string;
|
||||
replace_with_tag: string;
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
findNotes: {
|
||||
params: {
|
||||
query: IAnkiQuery;
|
||||
};
|
||||
result: number[];
|
||||
};
|
||||
notesInfo: {
|
||||
params: {
|
||||
notes: number[];
|
||||
};
|
||||
result: {
|
||||
noteId: number;
|
||||
modelName: string;
|
||||
tags: string[];
|
||||
fields: {
|
||||
[fieldName: string]: {
|
||||
value: string;
|
||||
ord: number;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
deleteNotes: {
|
||||
params: {
|
||||
notes: number[];
|
||||
};
|
||||
result: null;
|
||||
};
|
||||
removeEmptyNotes: {
|
||||
result: null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -236,6 +311,86 @@ export interface IAnkiConnectCardTemplate {
|
||||
Back: string;
|
||||
}
|
||||
|
||||
export interface IAnkiConnectNote {
|
||||
deckName: string;
|
||||
modelName: string;
|
||||
fields: {
|
||||
[fieldName: string]: string;
|
||||
};
|
||||
options?: {
|
||||
/**
|
||||
* The allowDuplicate member inside options group can be set to true to enable adding duplicate cards.
|
||||
* Normally duplicate cards can not be added and trigger exception.
|
||||
*/
|
||||
allowDuplicate: boolean;
|
||||
/**
|
||||
* The duplicateScope member inside options can be used to specify the scope for which duplicates are checked.
|
||||
* A value of "deck" will only check for duplicates in the target deck;
|
||||
* any other value will check the entire collection.
|
||||
*/
|
||||
duplicateScope?: string;
|
||||
duplicateScopeOptions?: {
|
||||
/**
|
||||
* Specify which deck to use for checking duplicates in.
|
||||
* If undefined or null, the target deck will be used.
|
||||
*/
|
||||
deckName?: string | null;
|
||||
/**
|
||||
* Change whether or not duplicate cards are checked in child decks.
|
||||
* The default value is false.
|
||||
*/
|
||||
checkChildren?: boolean;
|
||||
/**
|
||||
* Specifies whether duplicate checks are performed across all note types.
|
||||
* The default value is false.
|
||||
*/
|
||||
checkAllModel?: boolean;
|
||||
};
|
||||
};
|
||||
tags: string[];
|
||||
audio?: IAnkiConnectMediaInNote[];
|
||||
video?: IAnkiConnectMediaInNote[];
|
||||
picture?: IAnkiConnectMediaInNote[];
|
||||
}
|
||||
|
||||
export type IAnkiConnectMedia = (
|
||||
| {
|
||||
/**
|
||||
* Specified base64-encoded contents
|
||||
*/
|
||||
data: string;
|
||||
}
|
||||
| {
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
}
|
||||
) & {
|
||||
/**
|
||||
* To prevent Anki from removing files not used by any cards (e.g. for configuration files),
|
||||
* prefix the filename with an underscore.
|
||||
*/
|
||||
filename: string;
|
||||
/**
|
||||
* Any existing file with the same name is deleted by default.
|
||||
* Set `deleteExisting` to `false` to prevent that by letting Anki give the new file a non-conflicting name.
|
||||
*/
|
||||
deleteExisting?: boolean;
|
||||
};
|
||||
|
||||
export type IAnkiConnectMediaInNote = IAnkiConnectMedia & {
|
||||
/**
|
||||
* The skipHash field can be optionally provided to skip the inclusion of files with an MD5 hash that matches the provided value.
|
||||
*/
|
||||
skipHash?: string;
|
||||
/**
|
||||
* The fields member is a list of fields that should play audio or video,
|
||||
* or show a picture when the card is displayed in Anki.
|
||||
*/
|
||||
fields?: string[];
|
||||
};
|
||||
|
||||
export class AnkiConnect {
|
||||
$axios: AxiosInstance;
|
||||
|
||||
@@ -248,31 +403,8 @@ export class AnkiConnect {
|
||||
async api<A extends keyof IAnkiConnectActions>(
|
||||
action: A,
|
||||
params: IAnkiConnectActions[A]['params'],
|
||||
version?: number,
|
||||
): Promise<IAnkiConnectActions[A]['result']>;
|
||||
|
||||
async api<Results extends IAnkiConnectResponse<any>[]>(
|
||||
action: 'multi',
|
||||
params: {
|
||||
actions: Array<
|
||||
{
|
||||
[A in keyof IAnkiConnectActions]: {
|
||||
action: A;
|
||||
params: IAnkiConnectActions[A]['params'];
|
||||
};
|
||||
}[keyof IAnkiConnectActions]
|
||||
>;
|
||||
},
|
||||
version?: number,
|
||||
): Promise<{
|
||||
result: Results;
|
||||
}>;
|
||||
|
||||
async api<A extends keyof IAnkiConnectActions | 'multi'>(
|
||||
action: A,
|
||||
params: unknown,
|
||||
version = this.version,
|
||||
) {
|
||||
): Promise<IAnkiConnectActions[A]['result']> {
|
||||
const { data: response } = await this.$axios.post<{
|
||||
result: any;
|
||||
error: string | null;
|
||||
@@ -293,4 +425,46 @@ export class AnkiConnect {
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly-typed usage is via `[string, tuple]`:
|
||||
*
|
||||
* ```typescript
|
||||
* await new AnkiConnect().multi<['findNotes', 'getTags']>({
|
||||
* actions: [
|
||||
* {
|
||||
* action: 'findNotes',
|
||||
* params: {
|
||||
* query: 'added:1',
|
||||
* },
|
||||
* },
|
||||
* { action: 'getTags' }
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async multi<Actions extends (keyof IAnkiConnectActions)[]>(
|
||||
params: {
|
||||
actions: {
|
||||
[K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
|
||||
? {
|
||||
action: Actions[K];
|
||||
} & ('params' extends keyof IAnkiConnectActions[Actions[K]]
|
||||
? {
|
||||
params: IAnkiConnectActions[Actions[K]]['params'];
|
||||
}
|
||||
: {})
|
||||
: never;
|
||||
};
|
||||
},
|
||||
version = this.version,
|
||||
): Promise<{
|
||||
[K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions
|
||||
? // { result: IAnkiConnectActions[Actions[K]]['result']; error: string | null; }
|
||||
// On try-testing - error fields not returned, unlike in the docs.
|
||||
IAnkiConnectActions[Actions[K]]['result']
|
||||
: never;
|
||||
}> {
|
||||
return this.api('multi', params, version) as any;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user