@focus-reactive/payload-plugin-translator 0.8.0 → 0.8.1

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.
Files changed (42) hide show
  1. package/dist/composition/levels/useDocTranslationApi.d.ts +2 -2
  2. package/dist/composition/levels/useDocTranslationApi.js +3 -4
  3. package/dist/plugin.js +21 -74
  4. package/dist/server/features/createTranslationRoutes.d.ts +4 -13
  5. package/dist/server/features/createTranslationRoutes.js +3 -4
  6. package/dist/server/features/staleness/dismissStaleness.handler.js +6 -2
  7. package/dist/server/features/staleness/getDocumentStaleness.handler.js +2 -2
  8. package/dist/server/features/staleness/index.d.ts +1 -1
  9. package/dist/server/features/staleness/model.d.ts +4 -16
  10. package/dist/server/features/translate-document/handler.d.ts +6 -8
  11. package/dist/server/features/translate-document/handler.js +20 -46
  12. package/dist/server/features/translate-document/index.d.ts +1 -0
  13. package/dist/server/features/translate-document/index.js +1 -0
  14. package/dist/server/features/translate-document/wireTranslateRunner.d.ts +28 -0
  15. package/dist/server/features/translate-document/wireTranslateRunner.js +49 -0
  16. package/dist/server/modules/provenance/{provenanceCollection.d.ts → Provenance.collection.d.ts} +17 -9
  17. package/dist/server/modules/provenance/{provenanceCollection.js → Provenance.collection.js} +23 -9
  18. package/dist/server/modules/provenance/Provenance.service.d.ts +56 -0
  19. package/dist/server/modules/provenance/Provenance.service.js +124 -0
  20. package/dist/server/modules/provenance/Provenance.shapes.d.ts +26 -0
  21. package/dist/server/modules/provenance/Provenance.shapes.js +7 -0
  22. package/dist/server/modules/provenance/{PayloadProvenanceStore.js → Provenance.store.js} +1 -1
  23. package/dist/server/modules/provenance/Provenance.wiring.d.ts +23 -0
  24. package/dist/server/modules/provenance/Provenance.wiring.js +43 -0
  25. package/dist/server/modules/provenance/{provenanceCleanupHook.d.ts → ProvenanceCleanup.hook.d.ts} +4 -3
  26. package/dist/server/modules/provenance/{provenanceCleanupHook.js → ProvenanceCleanup.hook.js} +1 -1
  27. package/dist/server/modules/provenance/index.d.ts +8 -4
  28. package/dist/server/modules/provenance/index.js +6 -4
  29. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -14
  30. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -2
  31. package/dist/server/modules/translation-levels/index.d.ts +1 -1
  32. package/dist/server/modules/translation-levels/types.d.ts +20 -12
  33. package/dist/server/shared/payload/sourceDocument.d.ts +8 -0
  34. package/dist/server/shared/payload/sourceDocument.js +15 -0
  35. package/dist/types/ConfigModifier.d.ts +10 -0
  36. package/dist/types/ConfigModifier.js +10 -0
  37. package/package.json +1 -1
  38. package/dist/server/features/_lib/sourceDocument.d.ts +0 -9
  39. package/dist/server/features/_lib/sourceDocument.js +0 -16
  40. package/dist/server/features/staleness/service.d.ts +0 -15
  41. package/dist/server/features/staleness/service.js +0 -78
  42. /package/dist/server/modules/provenance/{PayloadProvenanceStore.d.ts → Provenance.store.d.ts} +0 -0
@@ -3,7 +3,7 @@ import type { LevelContext } from "../../server/modules/translation-levels";
3
3
  * Contribute the runner-agnostic document-translation API — the shared route
4
4
  * bundle, bound to the level context's runner. Both `documentLevel` and
5
5
  * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
- * path, so the bundle registers exactly once. The schema map and provenance
7
- * store factory ride along for the staleness endpoints.
6
+ * path, so the bundle registers exactly once. The provenance service factory
7
+ * rides along for the staleness endpoints (its schema lives inside the service).
8
8
  */
9
9
  export declare function useDocTranslationApi(ctx: LevelContext): void;
@@ -3,8 +3,8 @@ import { createTranslationRoutes } from "../../server/features/createTranslation
3
3
  * Contribute the runner-agnostic document-translation API — the shared route
4
4
  * bundle, bound to the level context's runner. Both `documentLevel` and
5
5
  * `collectionLevel` call this; the plugin deduplicates the endpoints by method +
6
- * path, so the bundle registers exactly once. The schema map and provenance
7
- * store factory ride along for the staleness endpoints.
6
+ * path, so the bundle registers exactly once. The provenance service factory
7
+ * rides along for the staleness endpoints (its schema lives inside the service).
8
8
  */ export function useDocTranslationApi(ctx) {
9
9
  ctx.addEndpoints(createTranslationRoutes({
10
10
  taskRunnerFactory: ctx.taskRunnerFactory,
@@ -13,8 +13,7 @@ import { createTranslationRoutes } from "../../server/features/createTranslation
13
13
  },
14
14
  access: ctx.access,
15
15
  basePath: ctx.basePath,
16
- schemaMap: ctx.schemaMap,
17
- provenanceStoreFactory: ctx.provenanceStoreFactory
16
+ provenanceServiceFactory: ctx.provenanceServiceFactory
18
17
  }));
19
18
  }
20
19
 
package/dist/plugin.js CHANGED
@@ -1,19 +1,9 @@
1
1
  import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
2
- import { DEFAULT_PROVENANCE_SLUG, PayloadProvenanceStore, assertProvenanceSlugFree, injectProvenanceCleanup, isProvenanceCollection, makeProvenanceCollection } from "./server/modules/provenance";
3
- import { TranslateDocumentHandler } from "./server/features/translate-document";
4
- import { LifecycleNotifier, taskFromHandlerInput, withQueuedNotification } from "./server/modules/lifecycle";
2
+ import { configureProvenance } from "./server/modules/provenance";
3
+ import { wireTranslateRunner } from "./server/features/translate-document";
5
4
  import { documentLevel, collectionLevel } from "./composition/levels";
6
5
  import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
7
6
  import { normalizePath } from "./server/shared";
8
- /**
9
- * Resolve the opt-in `provenance` config to a sidecar slug, or `null` when disabled.
10
- * `false`/omitted → off; `true` or `{}` → on with the default slug; `{ slug }` → on with that slug.
11
- */ function resolveProvenanceSlug(provenance) {
12
- if (!provenance) return null;
13
- if (provenance === true) return DEFAULT_PROVENANCE_SLUG;
14
- // `||` (not `??`) so an empty/blank slug falls back to the default instead of silently disabling.
15
- return provenance.slug || DEFAULT_PROVENANCE_SLUG;
16
- }
17
7
  /** @deprecated Use `translatorPlugin` function instead */ export class TranslateCollectionPlugin {
18
8
  pluginConfig;
19
9
  constructor(pluginConfig){
@@ -22,61 +12,30 @@ import { normalizePath } from "./server/shared";
22
12
  init() {
23
13
  return async (config)=>{
24
14
  const { access, translationProvider, runner, collections, levels, provenance, lifecycle, basePath: rawBasePath = "/translate" } = this.pluginConfig;
25
- const lifecycleCallbacks = lifecycle ?? {};
26
- // Build schema map from deep-cloned collections
27
- // Deep clone is required because Payload mutates the original collection objects,
28
- // removing `localized: true` from nested fields during sanitization.
29
- // We use JSON round-trip instead of structuredClone because Lexical editor
30
- // configs contain async functions that structuredClone cannot handle.
31
- // TODO: Consider introducing a FieldLike interface with only the properties
32
- // used by the pipeline (name, type, localized, fields, blocks, tabs, custom)
33
- // to make the contract explicit and avoid reliance on JSON round-trip.
15
+ // Build schema map from deep-cloned collections.
16
+ // Deep clone is required because Payload mutates the original collection objects, removing
17
+ // `localized: true` from nested fields during sanitization. JSON round-trip (not
18
+ // structuredClone) because Lexical editor configs contain async functions structuredClone
19
+ // cannot handle.
20
+ // TODO: Consider introducing a FieldLike interface with only the properties used by the
21
+ // pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
22
+ // and avoid reliance on JSON round-trip.
34
23
  const schemaMap = new Map(collections.map((col)=>[
35
24
  col.slug,
36
25
  JSON.parse(JSON.stringify(col.fields))
37
26
  ]));
38
27
  const collectionSlugs = new Set(schemaMap.keys());
39
28
  const basePath = normalizePath(rawBasePath);
40
- const provenanceSlug = resolveProvenanceSlug(provenance);
41
- if (provenanceSlug) {
42
- const existing = (config.collections ?? []).filter((collection)=>!isProvenanceCollection(collection));
43
- assertProvenanceSlugFree(provenanceSlug, existing);
44
- }
45
- const provenanceStoreFactory = provenanceSlug ? (p)=>new PayloadProvenanceStore(p, provenanceSlug) : undefined;
46
- const translateHandler = new TranslateDocumentHandler(translationProvider, schemaMap, provenanceStoreFactory);
47
- const runnerContext = {
48
- handler: async (payload, input)=>{
49
- const notifier = new LifecycleNotifier(lifecycleCallbacks, payload.logger);
50
- const task = taskFromHandlerInput(input);
51
- try {
52
- await translateHandler.handle(payload, {
53
- collection: input.collection,
54
- collectionId: input.collectionId,
55
- sourceLng: input.sourceLng,
56
- targetLng: input.targetLng,
57
- strategy: input.strategy,
58
- publishOnTranslation: input.publishOnTranslation
59
- });
60
- } catch (error) {
61
- await notifier.failed(task, error);
62
- throw error; // rethrow so the runner marks the job failed
63
- }
64
- await notifier.completed(task);
65
- },
29
+ // Each concern owns its own config-time wiring and exposes it uniformly; init() just composes.
30
+ const provenanceModule = configureProvenance(provenance, schemaMap);
31
+ const { taskRunnerFactory, configModifier: runnerConfigModifier } = wireTranslateRunner({
32
+ translationProvider,
33
+ schemaMap,
34
+ provenanceServiceFactory: provenanceModule.serviceFactory,
35
+ runner,
36
+ lifecycle: lifecycle ?? {},
66
37
  collections: Array.from(collectionSlugs)
67
- };
68
- const runnerConfigModifier = runner.configure(runnerContext);
69
- // Bind the context once so routes receive a self-sufficient factory: the
70
- // runner needs no mutable per-instance handler state and create() has no
71
- // "configure() must run first" ordering coupling (translator plan, 0c).
72
- // When an `onQueued` callback is set, decorate the runner so `enqueue` fires it.
73
- const taskRunnerFactory = {
74
- create: (payload)=>{
75
- const taskRunner = runner.create(payload, runnerContext.handler);
76
- if (!lifecycleCallbacks.onQueued) return taskRunner;
77
- return withQueuedNotification(taskRunner, new LifecycleNotifier(lifecycleCallbacks, payload.logger));
78
- }
79
- };
38
+ });
80
39
  const activeLevels = levels ?? [
81
40
  documentLevel(),
82
41
  collectionLevel()
@@ -88,23 +47,11 @@ import { normalizePath } from "./server/shared";
88
47
  taskRunnerFactory,
89
48
  schemaMap,
90
49
  translationProvider,
91
- provenanceStoreFactory
50
+ provenanceServiceFactory: provenanceModule.serviceFactory
92
51
  });
93
52
  for (const level of activeLevels)level.extend(builder);
94
53
  builder.addConfigModifier(runnerConfigModifier);
95
- if (provenanceSlug && provenanceStoreFactory) {
96
- builder.addConfigModifier((cfg)=>{
97
- const alreadyAdded = cfg.collections?.some((collection)=>collection.slug === provenanceSlug && isProvenanceCollection(collection));
98
- if (!alreadyAdded) {
99
- cfg.collections = [
100
- ...cfg.collections ?? [],
101
- makeProvenanceCollection(provenanceSlug)
102
- ];
103
- }
104
- injectProvenanceCleanup(cfg, collectionSlugs, provenanceStoreFactory, provenanceSlug);
105
- return cfg;
106
- });
107
- }
54
+ builder.addConfigModifier(provenanceModule.configure(collectionSlugs));
108
55
  builder.addAdminProvider(new CacheProviderExport(basePath));
109
56
  // The single place the Payload config is mutated.
110
57
  return builder.applyTo(config);
@@ -1,20 +1,11 @@
1
1
  import type { CollectionSlug, Endpoint } from "payload";
2
- import type { AccessGuard } from "../shared";
3
- import type { TaskRunnerFactory } from "../modules/task-runner";
4
- import type { CollectionSchemaMap } from "../../types/CollectionSchemaMap";
5
- import type { ProvenanceStoreFactory } from "../modules/provenance";
6
- export type TranslationRoutesDeps = {
7
- taskRunnerFactory: TaskRunnerFactory;
2
+ import type { TranslationContext } from "../modules/translation-levels";
3
+ export type TranslationRoutesDeps = Pick<TranslationContext, "taskRunnerFactory" | "access" | "provenanceServiceFactory"> & {
8
4
  /** Collections the plugin manages — gates which slugs the routes accept. */
9
5
  collectionConfig: {
10
6
  availableCollections: Set<CollectionSlug>;
11
7
  };
12
- access?: AccessGuard;
13
8
  basePath?: string;
14
- /** Original per-collection field schema — needed to recompute source fingerprints for staleness. */
15
- schemaMap: CollectionSchemaMap;
16
- /** Builds a provenance store; absent when provenance is disabled (staleness then reports empty). */
17
- provenanceStoreFactory?: ProvenanceStoreFactory;
18
9
  };
19
10
  /**
20
11
  * The translator's HTTP surface as one shared bundle: the job-API endpoints
@@ -26,6 +17,6 @@ export type TranslationRoutesDeps = {
26
17
  * document and collection levels each contribute this bundle via the level
27
18
  * context; the plugin deduplicates by method + path, so it registers exactly
28
19
  * once. The staleness endpoints always register; when provenance is disabled
29
- * (`provenanceStoreFactory` absent) they simply report no staleness.
20
+ * (`provenanceServiceFactory` absent) they simply report no staleness.
30
21
  */
31
- export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, schemaMap, provenanceStoreFactory, }: TranslationRoutesDeps): Endpoint[];
22
+ export declare function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, provenanceServiceFactory, }: TranslationRoutesDeps): Endpoint[];
@@ -15,12 +15,11 @@ import { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./
15
15
  * document and collection levels each contribute this bundle via the level
16
16
  * context; the plugin deduplicates by method + path, so it registers exactly
17
17
  * once. The staleness endpoints always register; when provenance is disabled
18
- * (`provenanceStoreFactory` absent) they simply report no staleness.
19
- */ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, schemaMap, provenanceStoreFactory }) {
18
+ * (`provenanceServiceFactory` absent) they simply report no staleness.
19
+ */ export function createTranslationRoutes({ taskRunnerFactory, collectionConfig, access, basePath, provenanceServiceFactory }) {
20
20
  const stalenessConfig = {
21
21
  availableCollections: collectionConfig.availableCollections,
22
- schemaMap,
23
- provenanceStoreFactory
22
+ provenanceServiceFactory
24
23
  };
25
24
  return [
26
25
  createEnqueueRoute(taskRunnerFactory, collectionConfig, access, basePath),
@@ -1,7 +1,6 @@
1
1
  import { ServerResponse } from "../../shared";
2
2
  import { isCollectionAvailable } from "../_lib/collection-utils";
3
3
  import { DismissStalenessInputSchema } from "./model";
4
- import { dismissLocaleStaleness } from "./service";
5
4
  /** Dismisses (acknowledges) staleness of one target locale for a document. */ export class DismissStalenessHandler {
6
5
  config;
7
6
  constructor(config){
@@ -17,7 +16,12 @@ import { dismissLocaleStaleness } from "./service";
17
16
  if (!collectionSlug) {
18
17
  return ServerResponse.badRequest("Collection not available for translation");
19
18
  }
20
- await dismissLocaleStaleness(req.payload, this.config, collectionSlug, collection_id, target_lng);
19
+ const service = this.config.provenanceServiceFactory?.(req.payload);
20
+ await service?.dismiss({
21
+ collectionSlug: collectionSlug,
22
+ documentId: collection_id,
23
+ targetLocale: target_lng
24
+ });
21
25
  return ServerResponse.success({
22
26
  success: true
23
27
  });
@@ -1,7 +1,6 @@
1
1
  import { ServerResponse } from "../../shared";
2
2
  import { isCollectionAvailable } from "../_lib/collection-utils";
3
3
  import { GetDocumentStalenessInputSchema } from "./model";
4
- import { computeDocumentStaleness } from "./service";
5
4
  /**
6
5
  * Reads per-locale staleness for a single document. Best-effort: a recompute failure (e.g. the
7
6
  * consumer enabled provenance but has not run the SQL migration, so the sidecar table is missing)
@@ -22,7 +21,8 @@ import { computeDocumentStaleness } from "./service";
22
21
  return ServerResponse.badRequest("Collection not available for translation");
23
22
  }
24
23
  try {
25
- const locales = await computeDocumentStaleness(req.payload, this.config, collectionSlug, collection_id);
24
+ const service = this.config.provenanceServiceFactory?.(req.payload);
25
+ const locales = service ? await service.getStaleness(collectionSlug, collection_id) : [];
26
26
  return ServerResponse.success({
27
27
  locales
28
28
  });
@@ -1,2 +1,2 @@
1
1
  export { createGetDocumentStalenessRoute, createDismissStalenessRoute } from "./route";
2
- export type { StalenessConfig, StalenessLocaleOutput } from "./model";
2
+ export type { StalenessConfig } from "./model";
@@ -1,7 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { CollectionSlug } from "payload";
3
- import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
4
- import type { ProvenanceStoreFactory } from "../../modules/provenance";
3
+ import type { TranslationContext } from "../../modules/translation-levels";
5
4
  /**
6
5
  * Route params for reading a document's per-locale staleness.
7
6
  *
@@ -32,21 +31,10 @@ export declare const DismissStalenessInputSchema: z.ZodObject<{
32
31
  target_lng: string;
33
32
  }>;
34
33
  /**
35
- * Per-locale staleness for one document (snake_case for client compatibility, matching the other
36
- * translation endpoints). One entry per target locale that has a provenance record.
37
- */
38
- export type StalenessLocaleOutput = {
39
- target_lng: string;
40
- source_lng: string;
41
- is_stale: boolean;
42
- translated_at: string;
43
- };
44
- /**
45
- * Handler configuration. `provenanceStoreFactory` is absent when provenance is disabled — the
34
+ * Handler configuration. `provenanceServiceFactory` is absent when provenance is disabled the
46
35
  * handlers then report no staleness (empty), so the endpoint contract stays stable either way.
36
+ * The fingerprint policy + schema live inside {@link ProvenanceService}, so no `schemaMap` here.
47
37
  */
48
- export type StalenessConfig = {
38
+ export type StalenessConfig = Pick<TranslationContext, "provenanceServiceFactory"> & {
49
39
  availableCollections: Set<CollectionSlug>;
50
- schemaMap: CollectionSchemaMap;
51
- provenanceStoreFactory?: ProvenanceStoreFactory;
52
40
  };
@@ -1,21 +1,19 @@
1
1
  import type { Payload } from "payload";
2
2
  import type { Handler } from "../../shared";
3
3
  import type { TranslationProvider } from "../../../core/translation-providers";
4
- import type { ProvenanceStoreFactory } from "../../modules/provenance";
4
+ import type { ProvenanceServiceFactory } from "../../modules/provenance";
5
5
  import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
6
6
  import type { TranslateDocumentInput, TranslateDocumentOutput } from "./model";
7
- export type TranslateDocumentDependencies = {
8
- translationProvider: TranslationProvider;
9
- schemaMap: CollectionSchemaMap;
10
- };
11
7
  /**
12
- * Translates a single document from source language to target language
8
+ * Translates a single document from source language to target language. Provenance is delegated to
9
+ * {@link ProvenanceService}: this handler only decides *when* to capture the source fingerprint
10
+ * (before the pipeline mutates the source in place) and *when* to record it (after the save).
13
11
  */
14
12
  export declare class TranslateDocumentHandler implements Handler<TranslateDocumentInput, TranslateDocumentOutput> {
15
13
  private readonly translationProvider;
16
14
  private readonly schemaMap;
17
- private readonly provenanceStoreFactory?;
18
- constructor(translationProvider: TranslationProvider, schemaMap: CollectionSchemaMap, provenanceStoreFactory?: ProvenanceStoreFactory);
15
+ private readonly provenanceServiceFactory?;
16
+ constructor(translationProvider: TranslationProvider, schemaMap: CollectionSchemaMap, provenanceServiceFactory?: ProvenanceServiceFactory);
19
17
  handle(payload: Payload, input: TranslateDocumentInput): Promise<TranslateDocumentOutput>;
20
18
  private saveTranslatedDocument;
21
19
  }
@@ -1,17 +1,18 @@
1
1
  import { APIError } from "payload";
2
2
  import { translateContent } from "../../../core/translation-pipeline";
3
- import { computeSourceFingerprint } from "../../../core/content-projection/computeSourceFingerprint";
4
- import { fetchSourceDocument } from "../_lib/sourceDocument";
3
+ import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
5
4
  /**
6
- * Translates a single document from source language to target language
5
+ * Translates a single document from source language to target language. Provenance is delegated to
6
+ * {@link ProvenanceService}: this handler only decides *when* to capture the source fingerprint
7
+ * (before the pipeline mutates the source in place) and *when* to record it (after the save).
7
8
  */ export class TranslateDocumentHandler {
8
9
  translationProvider;
9
10
  schemaMap;
10
- provenanceStoreFactory;
11
- constructor(translationProvider, schemaMap, provenanceStoreFactory){
11
+ provenanceServiceFactory;
12
+ constructor(translationProvider, schemaMap, provenanceServiceFactory){
12
13
  this.translationProvider = translationProvider;
13
14
  this.schemaMap = schemaMap;
14
- this.provenanceStoreFactory = provenanceStoreFactory;
15
+ this.provenanceServiceFactory = provenanceServiceFactory;
15
16
  }
16
17
  async handle(payload, input) {
17
18
  const { collection, collectionId, sourceLng, targetLng, strategy, publishOnTranslation } = input;
@@ -19,24 +20,12 @@ import { fetchSourceDocument } from "../_lib/sourceDocument";
19
20
  const schema = this.schemaMap.get(collection);
20
21
  if (!schema) throw new APIError(`Collection "${collection}" not found in schemaMap`, 400);
21
22
  const sourceData = await fetchSourceDocument(payload, collection, collectionId, sourceLng);
22
- // Capture the staleness baseline from the PRISTINE source NOW, before the pipeline runs. The
23
- // pipeline translates in place and shares object-valued source leaves (e.g. richText nodes) by
24
- // reference with `sourceData`, so fingerprinting after `translateContent` would hash the target
25
- // translation — making every fresh translation look immediately stale. Best-effort: a fingerprint
26
- // failure logs and skips provenance rather than breaking the translation.
27
- let sourceFingerprint = null;
28
- if (this.provenanceStoreFactory) {
29
- try {
30
- sourceFingerprint = computeSourceFingerprint(sourceData, schema);
31
- } catch (error) {
32
- payload.logger.error({
33
- err: error,
34
- collection,
35
- documentId: String(collectionId),
36
- msg: "translator: failed to fingerprint source for provenance"
37
- });
38
- }
39
- }
23
+ // Capture the staleness baseline from the PRISTINE source NOW, before the pipeline runs — it
24
+ // translates in place and shares object-valued source leaves (e.g. richText nodes) by reference,
25
+ // so fingerprinting after `translateContent` would hash the target translation and make every
26
+ // fresh translation look instantly stale. The service is best-effort (a failure returns null).
27
+ const provenance = this.provenanceServiceFactory?.(payload);
28
+ const sourceFingerprint = provenance?.captureFingerprint(collection, sourceData) ?? null;
40
29
  const targetData = await payload.findByID({
41
30
  collection,
42
31
  id: collectionId,
@@ -58,28 +47,13 @@ import { fetchSourceDocument } from "../_lib/sourceDocument";
58
47
  };
59
48
  const collectionConfig = payload.collections[collection].config;
60
49
  await this.saveTranslatedDocument(payload, collection, collectionId, translatedData, targetLng, sourceLng, collectionConfig, publishOnTranslation);
61
- if (this.provenanceStoreFactory && sourceFingerprint !== null) {
62
- const store = this.provenanceStoreFactory(payload);
63
- try {
64
- await store.upsert({
65
- collectionSlug: collection,
66
- documentId: String(collectionId),
67
- targetLocale: targetLng,
68
- sourceLocale: sourceLng,
69
- sourceFingerprint,
70
- translatedAt: new Date().toISOString(),
71
- dismissedFingerprint: null
72
- });
73
- } catch (error) {
74
- payload.logger.error({
75
- err: error,
76
- collection,
77
- documentId: String(collectionId),
78
- targetLocale: targetLng,
79
- sourceLocale: sourceLng,
80
- msg: "translator: failed to record translation provenance"
81
- });
82
- }
50
+ if (provenance && sourceFingerprint !== null) {
51
+ await provenance.record({
52
+ collectionSlug: collection,
53
+ documentId: String(collectionId),
54
+ targetLocale: targetLng,
55
+ sourceLocale: sourceLng
56
+ }, sourceFingerprint);
83
57
  }
84
58
  return {
85
59
  success: true
@@ -1,2 +1,3 @@
1
1
  export { TranslateDocumentHandler } from "./handler";
2
+ export { wireTranslateRunner } from "./wireTranslateRunner";
2
3
  export type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
@@ -1,3 +1,4 @@
1
1
  export { TranslateDocumentHandler } from "./handler";
2
+ export { wireTranslateRunner } from "./wireTranslateRunner";
2
3
 
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,28 @@
1
+ import type { CollectionSlug } from "payload";
2
+ import type { TranslationProvider } from "../../../core/translation-providers";
3
+ import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
4
+ import type { ConfigModifier } from "../../../types/ConfigModifier";
5
+ import type { ProvenanceServiceFactory } from "../../modules/provenance";
6
+ import type { TranslationLifecycleCallbacks } from "../../modules/lifecycle";
7
+ import type { TaskRunnerFactory, TaskRunnerProvider } from "../../modules/task-runner";
8
+ type WireTranslateRunnerParams = {
9
+ translationProvider: TranslationProvider;
10
+ schemaMap: CollectionSchemaMap;
11
+ provenanceServiceFactory?: ProvenanceServiceFactory;
12
+ runner: TaskRunnerProvider;
13
+ lifecycle: TranslationLifecycleCallbacks;
14
+ collections: CollectionSlug[];
15
+ };
16
+ /**
17
+ * Assemble the document-translation task pipeline: the {@link TranslateDocumentHandler}, the runner
18
+ * context that wraps each task with lifecycle notifications, the runner's config modifier, and the
19
+ * per-request {@link TaskRunnerFactory} (decorated with `onQueued` notification when configured).
20
+ *
21
+ * Extracted from the plugin's `init()` so the composition root stays a flat list — `plugin.ts` calls
22
+ * this once and registers the returned `configModifier` through the shared builder.
23
+ */
24
+ export declare function wireTranslateRunner({ translationProvider, schemaMap, provenanceServiceFactory, runner, lifecycle, collections, }: WireTranslateRunnerParams): {
25
+ taskRunnerFactory: TaskRunnerFactory;
26
+ configModifier: ConfigModifier;
27
+ };
28
+ export {};
@@ -0,0 +1,49 @@
1
+ import { LifecycleNotifier, taskFromHandlerInput, withQueuedNotification } from "../../modules/lifecycle";
2
+ import { TranslateDocumentHandler } from "./handler";
3
+ /**
4
+ * Assemble the document-translation task pipeline: the {@link TranslateDocumentHandler}, the runner
5
+ * context that wraps each task with lifecycle notifications, the runner's config modifier, and the
6
+ * per-request {@link TaskRunnerFactory} (decorated with `onQueued` notification when configured).
7
+ *
8
+ * Extracted from the plugin's `init()` so the composition root stays a flat list — `plugin.ts` calls
9
+ * this once and registers the returned `configModifier` through the shared builder.
10
+ */ export function wireTranslateRunner({ translationProvider, schemaMap, provenanceServiceFactory, runner, lifecycle, collections }) {
11
+ const translateHandler = new TranslateDocumentHandler(translationProvider, schemaMap, provenanceServiceFactory);
12
+ const runnerContext = {
13
+ handler: async (payload, input)=>{
14
+ const notifier = new LifecycleNotifier(lifecycle, payload.logger);
15
+ const task = taskFromHandlerInput(input);
16
+ try {
17
+ await translateHandler.handle(payload, {
18
+ collection: input.collection,
19
+ collectionId: input.collectionId,
20
+ sourceLng: input.sourceLng,
21
+ targetLng: input.targetLng,
22
+ strategy: input.strategy,
23
+ publishOnTranslation: input.publishOnTranslation
24
+ });
25
+ } catch (error) {
26
+ await notifier.failed(task, error);
27
+ throw error; // rethrow so the runner marks the job failed
28
+ }
29
+ await notifier.completed(task);
30
+ },
31
+ collections
32
+ };
33
+ // Bind the context once so routes receive a self-sufficient factory: the runner needs no mutable
34
+ // per-instance handler state, and create() has no "configure() must run first" ordering coupling.
35
+ // When an `onQueued` callback is set, decorate the runner so `enqueue` fires it.
36
+ const taskRunnerFactory = {
37
+ create: (payload)=>{
38
+ const taskRunner = runner.create(payload, runnerContext.handler);
39
+ if (!lifecycle.onQueued) return taskRunner;
40
+ return withQueuedNotification(taskRunner, new LifecycleNotifier(lifecycle, payload.logger));
41
+ }
42
+ };
43
+ return {
44
+ taskRunnerFactory,
45
+ configModifier: runner.configure(runnerContext)
46
+ };
47
+ }
48
+
49
+ //# sourceMappingURL=wireTranslateRunner.js.map
@@ -1,14 +1,7 @@
1
1
  import type { CollectionConfig } from "payload";
2
+ import type { ManagedCollectionsConfig } from "./Provenance.shapes";
2
3
  /** Default slug for the provenance sidecar collection. Overridable via `provenance.slug`. */
3
4
  export declare const DEFAULT_PROVENANCE_SLUG = "translator-provenance";
4
- /**
5
- * True for the plugin's own provenance sidecar collection. Recognised by the {@link PROVENANCE_MARKER}
6
- * on `custom` (not the slug, which is consumer-configurable), so plugin wiring can stay idempotent on
7
- * a repeated run and the slug-collision guard can ignore an already-added sidecar.
8
- */
9
- export declare function isProvenanceCollection(collection: {
10
- custom?: unknown;
11
- }): boolean;
12
5
  /**
13
6
  * Build the Payload config for the provenance sidecar collection.
14
7
  *
@@ -18,6 +11,21 @@ export declare function isProvenanceCollection(collection: {
18
11
  * content. The composite index on `(collectionSlug, documentId, targetLocale)` is the upsert key.
19
12
  *
20
13
  * The plugin ships only this config; the consumer's Payload creates the table (see the design doc's
21
- * SQL-migration note). Enabled only when the consumer opts in via `provenance` (wired in slice B).
14
+ * SQL-migration note). Enabled only when the consumer opts in via `provenance`.
22
15
  */
23
16
  export declare function makeProvenanceCollection(slug?: string): CollectionConfig;
17
+ /**
18
+ * True for the plugin's own provenance sidecar collection. Recognised by the {@link PROVENANCE_MARKER}
19
+ * on `custom` (not the slug, which is consumer-configurable), so plugin wiring can stay idempotent on
20
+ * a repeated run and the slug-collision guard can ignore an already-added sidecar.
21
+ */
22
+ export declare function isProvenanceCollection(collection: {
23
+ custom?: unknown;
24
+ }): boolean;
25
+ /**
26
+ * Idempotently register the provenance sidecar collection on `host`: add it once, skipping when a
27
+ * sidecar with this slug is already present, so a repeated `init()` never stacks a duplicate. Takes
28
+ * only the narrow {@link ManagedCollectionsConfig} slice — a real Payload `Config` is structurally
29
+ * assignable, and a test passes a plain `{ collections: [...] }` literal.
30
+ */
31
+ export declare function ensureProvenanceCollectionRegistered(host: ManagedCollectionsConfig, slug: string): void;
@@ -1,12 +1,5 @@
1
1
  /** Default slug for the provenance sidecar collection. Overridable via `provenance.slug`. */ export const DEFAULT_PROVENANCE_SLUG = "translator-provenance";
2
2
  /** The `custom` marker tagging the plugin's own provenance sidecar (set + read in this module). */ const PROVENANCE_MARKER = "translatorProvenance";
3
- /**
4
- * True for the plugin's own provenance sidecar collection. Recognised by the {@link PROVENANCE_MARKER}
5
- * on `custom` (not the slug, which is consumer-configurable), so plugin wiring can stay idempotent on
6
- * a repeated run and the slug-collision guard can ignore an already-added sidecar.
7
- */ export function isProvenanceCollection(collection) {
8
- return collection.custom?.[PROVENANCE_MARKER] === true;
9
- }
10
3
  /**
11
4
  * Build the Payload config for the provenance sidecar collection.
12
5
  *
@@ -16,7 +9,7 @@
16
9
  * content. The composite index on `(collectionSlug, documentId, targetLocale)` is the upsert key.
17
10
  *
18
11
  * The plugin ships only this config; the consumer's Payload creates the table (see the design doc's
19
- * SQL-migration note). Enabled only when the consumer opts in via `provenance` (wired in slice B).
12
+ * SQL-migration note). Enabled only when the consumer opts in via `provenance`.
20
13
  */ export function makeProvenanceCollection(slug = DEFAULT_PROVENANCE_SLUG) {
21
14
  return {
22
15
  slug,
@@ -78,5 +71,26 @@
78
71
  ]
79
72
  };
80
73
  }
74
+ /**
75
+ * True for the plugin's own provenance sidecar collection. Recognised by the {@link PROVENANCE_MARKER}
76
+ * on `custom` (not the slug, which is consumer-configurable), so plugin wiring can stay idempotent on
77
+ * a repeated run and the slug-collision guard can ignore an already-added sidecar.
78
+ */ export function isProvenanceCollection(collection) {
79
+ return collection.custom?.[PROVENANCE_MARKER] === true;
80
+ }
81
+ /**
82
+ * Idempotently register the provenance sidecar collection on `host`: add it once, skipping when a
83
+ * sidecar with this slug is already present, so a repeated `init()` never stacks a duplicate. Takes
84
+ * only the narrow {@link ManagedCollectionsConfig} slice — a real Payload `Config` is structurally
85
+ * assignable, and a test passes a plain `{ collections: [...] }` literal.
86
+ */ export function ensureProvenanceCollectionRegistered(host, slug) {
87
+ const alreadyAdded = host.collections?.some((collection)=>collection.slug === slug && isProvenanceCollection(collection));
88
+ if (!alreadyAdded) {
89
+ host.collections = [
90
+ ...host.collections ?? [],
91
+ makeProvenanceCollection(slug)
92
+ ];
93
+ }
94
+ }
81
95
 
82
- //# sourceMappingURL=provenanceCollection.js.map
96
+ //# sourceMappingURL=Provenance.collection.js.map