@jhb.software/payload-alt-text-plugin 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -18
- package/dist/endpoints/bulkGenerateAltTexts.js +8 -4
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +3 -1
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +19 -4
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/anthropic.d.ts +65 -0
- package/dist/resolvers/anthropic.js +141 -0
- package/dist/resolvers/anthropic.js.map +1 -0
- package/dist/resolvers/createVisionResolver.d.ts +148 -0
- package/dist/resolvers/createVisionResolver.js +300 -0
- package/dist/resolvers/createVisionResolver.js.map +1 -0
- package/dist/resolvers/mistral.d.ts +14 -1
- package/dist/resolvers/mistral.js +84 -250
- package/dist/resolvers/mistral.js.map +1 -1
- package/dist/resolvers/openAI.d.ts +22 -3
- package/dist/resolvers/openAI.js +57 -138
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +47 -11
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.d.ts +3 -1
- package/dist/utilities/altTextHealth.js +74 -8
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/stableStringify.d.ts +9 -0
- package/dist/utilities/stableStringify.js +19 -0
- package/dist/utilities/stableStringify.js.map +1 -0
- package/package.json +4 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n 'ALT_TEXT_COLLECTION_READ_FAILED' | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n): Promise<{ alt: unknown; id: number | string }[]> {\n const where = buildMimeTypeWhere(mimeTypes)\n if (!where) {\n return []\n }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes)\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\nasync function getAltTextHealthScan(req: PayloadRequest): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const localeCodes =\n localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [pluginConfig.locale] : [])\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes,\n })\n }\n\n const collections = pluginConfig.collections\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory: unstable_cache,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","fetchAllDocs","payload","mimeTypes","where","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","getAltTextHealthScan","req","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","cacheFactory","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","length","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\nimport type { AltTextHealthCacheFactory } from './altTextHealthCache.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { stableStringify } from './stableStringify.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n | 'ALT_TEXT_BASE_FILTER_FAILED'\n | 'ALT_TEXT_COLLECTION_READ_FAILED'\n | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n /** The resolved base filter per collection slug, if one is configured. */\n baseFilters: Record<string, undefined | Where>\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nconst isEmptyWhere = (where: undefined | Where): boolean =>\n !where || Object.keys(where).length === 0\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n baseFilter: undefined | Where,\n): Promise<{ alt: unknown; id: number | string }[]> {\n const mimeTypeWhere = buildMimeTypeWhere(mimeTypes)\n if (!mimeTypeWhere) {\n return []\n }\n\n // The scan runs with `overrideAccess: true`, so a narrowing constraint has to be\n // part of the query itself — this is what keeps the aggregate within one tenant.\n const where = isEmptyWhere(baseFilter) ? mimeTypeWhere : { and: [mimeTypeWhere, baseFilter!] }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes, baseFilters[slug])\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\n/**\n * Resolves the configured base filter for every scanned collection.\n *\n * A throwing filter (a tenant cookie pointing at a deleted tenant, say) must not\n * take the dashboard down, and must never fall back to an unfiltered scan — so it\n * ends the scan with an error the widget and the endpoint report. The error carries\n * the slug in its message rather than in `collection`, so it survives the read-access\n * filter that drops errors for collections the caller cannot read.\n */\nasync function resolveBaseFilters(\n req: PayloadRequest,\n collections: NormalizedAltTextCollectionConfig[],\n baseFilter: AltTextPluginConfig['healthCheckBaseFilter'],\n): Promise<{ baseFilters: Record<string, undefined | Where> } | { error: AltTextHealthError }> {\n const baseFilters: Record<string, undefined | Where> = {}\n\n if (!baseFilter) {\n return { baseFilters }\n }\n\n for (const { slug } of collections) {\n try {\n baseFilters[slug] = await baseFilter({ collection: slug, req })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n\n req.payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while resolving the base filter.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n error: {\n code: 'ALT_TEXT_BASE_FILTER_FAILED',\n message: `Failed to resolve the alt text health base filter for the \"${slug}\" collection: ${message}`,\n },\n }\n }\n }\n\n return { baseFilters }\n}\n\nexport async function getAltTextHealthScan(\n req: PayloadRequest,\n cacheFactory: AltTextHealthCacheFactory<AltTextHealthScan> = unstable_cache,\n): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const localeCodes =\n localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [pluginConfig.locale] : [])\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes,\n })\n }\n\n const collections = pluginConfig.collections\n\n const resolved = await resolveBaseFilters(req, collections, pluginConfig.healthCheckBaseFilter)\n\n if ('error' in resolved) {\n return createUnknownScan({ error: resolved.error, isLocalized, localeCodes })\n }\n\n const { baseFilters } = resolved\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n // The scan is shared across requests, so a scoped scan needs a scoped cache\n // entry. Deriving the key from the resolved filters — rather than taking one\n // from the caller — makes it impossible to narrow the scan without also\n // narrowing its cache, which would serve one tenant's counts to another.\n `filter:${stableStringify(baseFilters)}`,\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n baseFilters,\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","stableStringify","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","isEmptyWhere","where","Object","keys","length","fetchAllDocs","payload","mimeTypes","baseFilter","mimeTypeWhere","and","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","baseFilters","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","resolveBaseFilters","req","getAltTextHealthScan","cacheFactory","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","resolved","healthCheckBaseFilter","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes a value with object keys sorted, so two structurally equal values
|
|
3
|
+
* produce the same string regardless of the order their keys were written in.
|
|
4
|
+
*
|
|
5
|
+
* The health scan uses it to derive a cache key from the resolved base filter:
|
|
6
|
+
* narrowing the scan then always narrows its cache entry, which no amount of
|
|
7
|
+
* documentation can guarantee for a caller-supplied key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stableStringify(value: unknown): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes a value with object keys sorted, so two structurally equal values
|
|
3
|
+
* produce the same string regardless of the order their keys were written in.
|
|
4
|
+
*
|
|
5
|
+
* The health scan uses it to derive a cache key from the resolved base filter:
|
|
6
|
+
* narrowing the scan then always narrows its cache entry, which no amount of
|
|
7
|
+
* documentation can guarantee for a caller-supplied key.
|
|
8
|
+
*/ export function stableStringify(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
const entries = Object.entries(value).filter(([, entryValue])=>entryValue !== undefined).sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0).map(([key, entryValue])=>`${JSON.stringify(key)}:${stableStringify(entryValue)}`);
|
|
14
|
+
return `{${entries.join(',')}}`;
|
|
15
|
+
}
|
|
16
|
+
return JSON.stringify(value) ?? 'null';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=stableStringify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/stableStringify.ts"],"sourcesContent":["/**\n * Serializes a value with object keys sorted, so two structurally equal values\n * produce the same string regardless of the order their keys were written in.\n *\n * The health scan uses it to derive a cache key from the resolved base filter:\n * narrowing the scan then always narrows its cache entry, which no amount of\n * documentation can guarantee for a caller-supplied key.\n */\nexport function stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(',')}]`\n }\n\n if (value && typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)\n\n return `{${entries.join(',')}}`\n }\n\n return JSON.stringify(value) ?? 'null'\n}\n"],"names":["stableStringify","value","Array","isArray","map","join","entries","Object","filter","entryValue","undefined","sort","a","b","key","JSON","stringify"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,SAASA,gBAAgBC,KAAc;IAC5C,IAAIC,MAAMC,OAAO,CAACF,QAAQ;QACxB,OAAO,CAAC,CAAC,EAAEA,MAAMG,GAAG,CAACJ,iBAAiBK,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD;IAEA,IAAIJ,SAAS,OAAOA,UAAU,UAAU;QACtC,MAAMK,UAAUC,OAAOD,OAAO,CAACL,OAC5BO,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,eAAeC,WAC1CC,IAAI,CAAC,CAAC,CAACC,EAAE,EAAE,CAACC,EAAE,GAAMD,IAAIC,IAAI,CAAC,IAAID,IAAIC,IAAI,IAAI,GAC7CT,GAAG,CAAC,CAAC,CAACU,KAAKL,WAAW,GAAK,GAAGM,KAAKC,SAAS,CAACF,KAAK,CAAC,EAAEd,gBAAgBS,aAAa;QAErF,OAAO,CAAC,CAAC,EAAEH,QAAQD,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC;IAEA,OAAOU,KAAKC,SAAS,CAACf,UAAU;AAClC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhb.software/payload-alt-text-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A Payload CMS plugin that adds AI-powered alt text generation for images.",
|
|
5
5
|
"bugs": "https://github.com/jhb-software/payload-plugins/issues",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"main": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"openai": "^7.4.0",
|
|
24
23
|
"p-map": "^7.0.6",
|
|
25
24
|
"zod": "^4.4.3"
|
|
26
25
|
},
|
|
@@ -36,15 +35,15 @@
|
|
|
36
35
|
"@payloadcms/eslint-config": "^3.28.0",
|
|
37
36
|
"@payloadcms/translations": "^3.88.0",
|
|
38
37
|
"@swc/cli": "^0.8.1",
|
|
39
|
-
"@swc/core": "^1.
|
|
38
|
+
"@swc/core": "^1.16.1",
|
|
40
39
|
"@types/react": "19.2.18",
|
|
41
|
-
"@types/react-dom": "19.2.
|
|
40
|
+
"@types/react-dom": "19.2.5",
|
|
42
41
|
"copyfiles": "2.4.1",
|
|
43
42
|
"eslint": "^9.39.4",
|
|
44
43
|
"prettier": "3.9.6",
|
|
45
44
|
"rimraf": "6.1.3",
|
|
46
45
|
"typescript": "^6.0.3",
|
|
47
|
-
"vitest": "^4.1.
|
|
46
|
+
"vitest": "^4.1.11"
|
|
48
47
|
},
|
|
49
48
|
"files": [
|
|
50
49
|
"dist"
|