mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat: plugin-sdk safety all around (#29323)
This commit is contained in:
parent
cb1af3a8ec
commit
688241a462
11 changed files with 200 additions and 184 deletions
|
|
@ -5,8 +5,8 @@
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm build:tsc && pnpm build:wasm",
|
"build": "pnpm build:tsc && pnpm build:wasm",
|
||||||
"build:tsc": "mkdir -p dist && echo \"type Manifest = $(cat manifest.json); \nexport default Manifest;\" > dist/manifest.d.ts && tsc --noEmit && node esbuild.js",
|
"build:tsc": "plugin-sdk prepareBuild && tsc --noEmit && node esbuild.js",
|
||||||
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
|
"build:wasm": "extism-js dist/index.js -i dist/index.d.ts -o dist/plugin.wasm"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
||||||
27
packages/plugin-core/src/index.d.ts
vendored
27
packages/plugin-core/src/index.d.ts
vendored
|
|
@ -1,27 +0,0 @@
|
||||||
// keep in sync with plugin-sdk/host-functions.ts';
|
|
||||||
declare module 'extism:host' {
|
|
||||||
interface user {
|
|
||||||
searchAlbums(ptr: PTR): I64;
|
|
||||||
createAlbum(ptr: PTR): I64;
|
|
||||||
addAssetsToAlbum(ptr: PTR): I64;
|
|
||||||
addAssetsToAlbums(ptr: PTR): I64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep in sync with manifest.json
|
|
||||||
declare module 'main' {
|
|
||||||
// filters
|
|
||||||
export function assetFileFilter(): I32;
|
|
||||||
export function assetMissingTimeZoneFilter(): I32;
|
|
||||||
export function assetLocationFilter(): I32;
|
|
||||||
export function assetTypeFilter(): I32;
|
|
||||||
|
|
||||||
// updates
|
|
||||||
export function assetFavorite(): I32;
|
|
||||||
export function assetVisibility(): I32;
|
|
||||||
export function assetArchive(): I32;
|
|
||||||
export function assetLock(): I32;
|
|
||||||
export function assetTimeline(): I32;
|
|
||||||
// export function assetTrash(): I32;
|
|
||||||
export function assetAddToAlbums(): I32;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { getWrapper } from '@immich/plugin-sdk';
|
import { getWrapper } from '@immich/plugin-sdk';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import type manifestType from '../dist/manifest';
|
import type { Manifest } from '../dist/index.d.ts';
|
||||||
|
|
||||||
const wrapper = getWrapper<manifestType>();
|
const wrapper = getWrapper<Manifest>();
|
||||||
|
|
||||||
export const assetFileFilter = () => {
|
export const assetFileFilter = wrapper<'assetFileFilter'>(({ data, config }) => {
|
||||||
return wrapper<'assetFileFilter'>(({ data, config }) => {
|
|
||||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||||
|
|
||||||
const { asset } = data;
|
const { asset } = data;
|
||||||
|
|
@ -37,19 +36,15 @@ export const assetFileFilter = () => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetMissingTimeZoneFilter = () => {
|
export const assetMissingTimeZoneFilter = wrapper<'assetMissingTimeZoneFilter'>(({ config, data }) => {
|
||||||
return wrapper<'assetMissingTimeZoneFilter'>(({ config, data }) => {
|
|
||||||
const hasTimeZone = !!data.asset?.exifInfo?.timeZone;
|
const hasTimeZone = !!data.asset?.exifInfo?.timeZone;
|
||||||
const needsTimeZone = config.inverse ? true : false;
|
const needsTimeZone = config.inverse ? true : false;
|
||||||
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
return { workflow: { continue: hasTimeZone === needsTimeZone } };
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetLocationFilter = () => {
|
export const assetLocationFilter = wrapper<'assetLocationFilter'>(({ config, data }) => {
|
||||||
return wrapper<'assetLocationFilter'>(({ config, data }) => {
|
|
||||||
if (
|
if (
|
||||||
(config.region?.country && config.region.country !== data.asset.exifInfo?.country) ||
|
(config.region?.country && config.region.country !== data.asset.exifInfo?.country) ||
|
||||||
(config.region?.state && config.region.state !== data.asset.exifInfo?.state) ||
|
(config.region?.state && config.region.state !== data.asset.exifInfo?.state) ||
|
||||||
|
|
@ -84,17 +79,13 @@ export const assetLocationFilter = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return { workflow: { continue: earthDiameter * delta <= (config.coordinate?.radius ?? 0) } };
|
return { workflow: { continue: earthDiameter * delta <= (config.coordinate?.radius ?? 0) } };
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetTypeFilter = () => {
|
export const assetTypeFilter = wrapper<'assetTypeFilter'>(({ config, data }) => {
|
||||||
return wrapper<'assetTypeFilter'>(({ config, data }) => {
|
|
||||||
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetFavorite = () => {
|
export const assetFavorite = wrapper<'assetFavorite'>(({ config, data }) => {
|
||||||
return wrapper<'assetFavorite'>(({ config, data }) => {
|
|
||||||
const target = config.inverse ? false : true;
|
const target = config.inverse ? false : true;
|
||||||
if (target !== data.asset.isFavorite) {
|
if (target !== data.asset.isFavorite) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -103,17 +94,13 @@ export const assetFavorite = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetVisibility = () => {
|
export const assetVisibility = wrapper<'assetVisibility'>(({ config }) => ({
|
||||||
return wrapper<'assetVisibility'>(({ config }) => ({
|
|
||||||
changes: { asset: { visibility: config.visibility as AssetVisibility } },
|
changes: { asset: { visibility: config.visibility as AssetVisibility } },
|
||||||
}));
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
export const assetArchive = () => {
|
export const assetArchive = wrapper<'assetArchive'>(({ config, data }) => {
|
||||||
return wrapper<'assetArchive'>(({ config, data }) => {
|
|
||||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Archive) {
|
if (!config.inverse && data.asset.visibility !== AssetVisibility.Archive) {
|
||||||
return { changes: { asset: { visibility: AssetVisibility.Archive } } };
|
return { changes: { asset: { visibility: AssetVisibility.Archive } } };
|
||||||
}
|
}
|
||||||
|
|
@ -123,11 +110,9 @@ export const assetArchive = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const assetLock = () => {
|
export const assetLock = wrapper<'assetLock'>(({ config, data }) => {
|
||||||
return wrapper<'assetLock'>(({ config, data }) => {
|
|
||||||
if (!config.inverse && data.asset.visibility !== AssetVisibility.Locked) {
|
if (!config.inverse && data.asset.visibility !== AssetVisibility.Locked) {
|
||||||
return { changes: { asset: { visibility: AssetVisibility.Locked } } };
|
return { changes: { asset: { visibility: AssetVisibility.Locked } } };
|
||||||
}
|
}
|
||||||
|
|
@ -137,16 +122,14 @@ export const assetLock = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// export const assetTrash = () => {
|
// export const assetTrash = () => {
|
||||||
// // TODO use trash/untrash host functions
|
// // TODO use trash/untrash host functions
|
||||||
// return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
// return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
||||||
// };
|
// };
|
||||||
|
|
||||||
export const assetAddToAlbums = () => {
|
export const assetAddToAlbums = wrapper<'assetAddToAlbums'>(({ config, data, functions }) => {
|
||||||
return wrapper<'assetAddToAlbums'>(({ config, data, functions }) => {
|
|
||||||
const assetId = data.asset.id;
|
const assetId = data.asset.id;
|
||||||
|
|
||||||
if (config.albumIds.length === 0) {
|
if (config.albumIds.length === 0) {
|
||||||
|
|
@ -171,5 +154,4 @@ export const assetAddToAlbums = () => {
|
||||||
|
|
||||||
functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [assetId] });
|
functions.addAssetsToAlbums({ albumIds: config.albumIds, assetIds: [assetId] });
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"skipLibCheck": true, // Skip type checking of declaration files
|
"skipLibCheck": true, // Skip type checking of declaration files
|
||||||
"strict": true, // Enable all strict type-checking options
|
"strict": true, // Enable all strict type-checking options
|
||||||
"target": "es2020", // Specify ECMAScript target version
|
"target": "es2020", // Specify ECMAScript target version
|
||||||
"types": ["./src/index.d.ts", "./node_modules/@extism/js-pdk"] // Specify a list of type definition files to be included in the compilation
|
"types": ["./dist/index.d.ts", "./node_modules/@extism/js-pdk"] // Specify a list of type definition files to be included in the compilation
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules" // Exclude the node_modules directory
|
"node_modules" // Exclude the node_modules directory
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
entryPoints: ['src/index.ts'],
|
entryPoints: ['src/index.ts', 'src/cli.ts'],
|
||||||
outdir: 'dist',
|
outdir: 'dist',
|
||||||
bundle: true,
|
bundle: true,
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: false,
|
minify: false,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
|
platform: 'node',
|
||||||
target: ['es2020'],
|
target: ['es2020'],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"bin": {
|
||||||
|
"plugin-sdk": "./plugin-sdk.mjs"
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
|
|
@ -35,5 +38,8 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@extism/js-pdk": "^1.1.1"
|
"@extism/js-pdk": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^15.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
packages/plugin-sdk/plugin-sdk.mjs
Executable file
2
packages/plugin-sdk/plugin-sdk.mjs
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import "./dist/cli.js";
|
||||||
43
packages/plugin-sdk/src/cli.ts
Normal file
43
packages/plugin-sdk/src/cli.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
|
import { availableFunctions } from 'src/host-functions.js';
|
||||||
|
|
||||||
|
const program = new Command('plugin-sdk');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('prepareBuild')
|
||||||
|
.description('Generate .d.ts file required for extism')
|
||||||
|
.argument(
|
||||||
|
'[manifest]',
|
||||||
|
"Path to the plugins's manifest file",
|
||||||
|
'manifest.json',
|
||||||
|
)
|
||||||
|
.option('-o --output', 'Output file for generated types', 'dist/index.d.ts')
|
||||||
|
.action((manifest: string, { output }) => {
|
||||||
|
const content = readFileSync(manifest, { encoding: 'utf-8' });
|
||||||
|
const methods = (
|
||||||
|
JSON.parse(content) as { methods: { name: string }[] }
|
||||||
|
).methods.map(({ name }) => name);
|
||||||
|
mkdirSync(dirname(output), { recursive: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
output,
|
||||||
|
`
|
||||||
|
declare module 'extism:host' {
|
||||||
|
interface user {
|
||||||
|
${availableFunctions.map((functionName) => ` ${functionName}(ptr: PTR): I64;`).join('\n')}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'main' {
|
||||||
|
${methods.map((method) => ` export function ${method}(): I32;`).join('\n')}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Manifest = ${content};
|
||||||
|
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
|
|
@ -6,14 +6,11 @@ import {
|
||||||
type CreateAlbumDto,
|
type CreateAlbumDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
|
||||||
// keep in sync with plugin-core/src/index.d.ts';
|
|
||||||
declare module 'extism:host' {
|
declare module 'extism:host' {
|
||||||
interface user {
|
interface user extends Record<
|
||||||
searchAlbums(ptr: PTR): I64;
|
(typeof availableFunctions)[number],
|
||||||
createAlbum(ptr: PTR): I64;
|
(ptr: PTR) => I64
|
||||||
addAssetsToAlbum(ptr: PTR): I64;
|
> {}
|
||||||
addAssetsToAlbums(ptr: PTR): I64;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlbumsToAssets = {
|
type AlbumsToAssets = {
|
||||||
|
|
@ -34,6 +31,13 @@ type HostFunctionResult<T> =
|
||||||
type QueryParams<T extends (...args: any) => any> = Parameters<T>[0];
|
type QueryParams<T extends (...args: any) => any> = Parameters<T>[0];
|
||||||
type AlbumSearchDto = QueryParams<typeof getAllAlbums>;
|
type AlbumSearchDto = QueryParams<typeof getAllAlbums>;
|
||||||
|
|
||||||
|
export const availableFunctions = [
|
||||||
|
'searchAlbums',
|
||||||
|
'createAlbum',
|
||||||
|
'addAssetsToAlbum',
|
||||||
|
'addAssetsToAlbums',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const hostFunctions = (authToken: string) => {
|
export const hostFunctions = (authToken: string) => {
|
||||||
const host = Host.getFunctions();
|
const host = Host.getFunctions();
|
||||||
type HostFunctionName = keyof typeof host;
|
type HostFunctionName = keyof typeof host;
|
||||||
|
|
@ -75,5 +79,5 @@ export const hostFunctions = (authToken: string) => {
|
||||||
),
|
),
|
||||||
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
||||||
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
||||||
};
|
} satisfies Record<(typeof availableFunctions)[number], unknown>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ export const getWrapper =
|
||||||
functions: ReturnType<typeof hostFunctions>;
|
functions: ReturnType<typeof hostFunctions>;
|
||||||
},
|
},
|
||||||
) => WorkflowResponse<L> | undefined,
|
) => WorkflowResponse<L> | undefined,
|
||||||
) => {
|
) =>
|
||||||
|
() => {
|
||||||
const input = Host.inputString();
|
const input = Host.inputString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
|
|
@ -336,6 +336,10 @@ importers:
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
|
|
||||||
packages/plugin-sdk:
|
packages/plugin-sdk:
|
||||||
|
dependencies:
|
||||||
|
commander:
|
||||||
|
specifier: ^15.0.0
|
||||||
|
version: 15.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@extism/js-pdk':
|
'@extism/js-pdk':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue