@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
@@ -0,0 +1,56 @@
1
+ import type { CollectionSlug, Payload } from "payload";
2
+ import type { ProvenanceKey, ProvenanceStore } from "../../../core/provenance";
3
+ import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
4
+ /** Per-locale staleness for one document (snake_case, matching the other translation endpoints). */
5
+ export type StalenessLocale = {
6
+ target_lng: string;
7
+ source_lng: string;
8
+ is_stale: boolean;
9
+ translated_at: string;
10
+ };
11
+ /** Builds a {@link ProvenanceService} bound to a Payload instance; absent when provenance is disabled. */
12
+ export type ProvenanceServiceFactory = (payload: Payload) => ProvenanceService;
13
+ /**
14
+ * The single owner of provenance fingerprint policy — how the source is hashed on write, re-hashed on
15
+ * read, and compared for staleness. The write path and the read path go through this one class, so
16
+ * they can never drift (the biggest correctness trap in staleness detection). Sits above the CRUD
17
+ * {@link ProvenanceStore} port; the port + `computeSourceFingerprint` + `isRecordStale` stay
18
+ * framework-agnostic in the core.
19
+ *
20
+ * Best-effort by contract: fingerprint/record failures log and no-op rather than failing a translation.
21
+ */
22
+ export declare class ProvenanceService {
23
+ private readonly payload;
24
+ private readonly store;
25
+ private readonly schemaMap;
26
+ constructor(payload: Payload, store: ProvenanceStore, schemaMap: CollectionSchemaMap);
27
+ /**
28
+ * Hash the PRISTINE source. The caller MUST pass source fetched **before** the translation pipeline
29
+ * runs — the pipeline mutates object-valued leaves (e.g. richText nodes) in place, so hashing after
30
+ * it would capture the target translation and make every fresh translation look instantly stale.
31
+ * Returns `null` on any failure (no schema, hashing error) so provenance is skipped, not the translation.
32
+ */
33
+ captureFingerprint(collection: CollectionSlug, sourceData: Record<string, unknown>): string | null;
34
+ /** Persist a translation receipt (best-effort; a store failure logs and no-ops). */
35
+ record(key: ProvenanceKey & {
36
+ sourceLocale: string;
37
+ }, sourceFingerprint: string): Promise<void>;
38
+ /**
39
+ * Per-locale staleness for one document: read every receipt, recompute the current source
40
+ * fingerprint (write-path-identical), and mark each locale stale on undismissed drift. Returns `[]`
41
+ * when the collection has no schema. Isolates per-locale failures so one bad record can't blank the rest.
42
+ */
43
+ getStaleness(collection: CollectionSlug, documentId: string): Promise<StalenessLocale[]>;
44
+ /**
45
+ * Acknowledge the current source drift for one target locale: persist the current fingerprint as the
46
+ * dismissed one, so the indicator hides until the source changes again. No-op when the collection has
47
+ * no schema or the locale has no record.
48
+ */
49
+ dismiss(key: ProvenanceKey): Promise<void>;
50
+ /**
51
+ * Recompute the current source fingerprint the same way the write path does (shared fetch shape +
52
+ * hash). Cached per source locale so a document translated from one source into N locales fetches
53
+ * the source once.
54
+ */
55
+ private makeCurrentFingerprint;
56
+ }
@@ -0,0 +1,124 @@
1
+ import { computeSourceFingerprint } from "../../../core/content-projection/computeSourceFingerprint";
2
+ import { isRecordStale } from "../../../core/provenance";
3
+ import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
4
+ /**
5
+ * The single owner of provenance fingerprint policy — how the source is hashed on write, re-hashed on
6
+ * read, and compared for staleness. The write path and the read path go through this one class, so
7
+ * they can never drift (the biggest correctness trap in staleness detection). Sits above the CRUD
8
+ * {@link ProvenanceStore} port; the port + `computeSourceFingerprint` + `isRecordStale` stay
9
+ * framework-agnostic in the core.
10
+ *
11
+ * Best-effort by contract: fingerprint/record failures log and no-op rather than failing a translation.
12
+ */ export class ProvenanceService {
13
+ payload;
14
+ store;
15
+ schemaMap;
16
+ constructor(payload, store, schemaMap){
17
+ this.payload = payload;
18
+ this.store = store;
19
+ this.schemaMap = schemaMap;
20
+ }
21
+ /**
22
+ * Hash the PRISTINE source. The caller MUST pass source fetched **before** the translation pipeline
23
+ * runs — the pipeline mutates object-valued leaves (e.g. richText nodes) in place, so hashing after
24
+ * it would capture the target translation and make every fresh translation look instantly stale.
25
+ * Returns `null` on any failure (no schema, hashing error) so provenance is skipped, not the translation.
26
+ */ captureFingerprint(collection, sourceData) {
27
+ const schema = this.schemaMap.get(collection);
28
+ if (!schema) return null;
29
+ try {
30
+ return computeSourceFingerprint(sourceData, schema);
31
+ } catch (error) {
32
+ this.payload.logger.error({
33
+ err: error,
34
+ collection,
35
+ msg: "translator: failed to fingerprint source for provenance"
36
+ });
37
+ return null;
38
+ }
39
+ }
40
+ /** Persist a translation receipt (best-effort; a store failure logs and no-ops). */ async record(key, sourceFingerprint) {
41
+ try {
42
+ await this.store.upsert({
43
+ collectionSlug: key.collectionSlug,
44
+ documentId: key.documentId,
45
+ targetLocale: key.targetLocale,
46
+ sourceLocale: key.sourceLocale,
47
+ sourceFingerprint,
48
+ translatedAt: new Date().toISOString(),
49
+ dismissedFingerprint: null
50
+ });
51
+ } catch (error) {
52
+ this.payload.logger.error({
53
+ err: error,
54
+ collection: key.collectionSlug,
55
+ documentId: key.documentId,
56
+ targetLocale: key.targetLocale,
57
+ sourceLocale: key.sourceLocale,
58
+ msg: "translator: failed to record translation provenance"
59
+ });
60
+ }
61
+ }
62
+ /**
63
+ * Per-locale staleness for one document: read every receipt, recompute the current source
64
+ * fingerprint (write-path-identical), and mark each locale stale on undismissed drift. Returns `[]`
65
+ * when the collection has no schema. Isolates per-locale failures so one bad record can't blank the rest.
66
+ */ async getStaleness(collection, documentId) {
67
+ const schema = this.schemaMap.get(collection);
68
+ if (!schema) return [];
69
+ const records = await this.store.findByDocument(collection, documentId);
70
+ if (records.length === 0) return [];
71
+ const currentFingerprint = this.makeCurrentFingerprint(collection, documentId, schema);
72
+ const locales = [];
73
+ for (const record of records){
74
+ try {
75
+ const current = await currentFingerprint(record.sourceLocale);
76
+ locales.push({
77
+ target_lng: record.targetLocale,
78
+ source_lng: record.sourceLocale,
79
+ is_stale: isRecordStale(record, current),
80
+ translated_at: record.translatedAt
81
+ });
82
+ } catch (error) {
83
+ this.payload.logger.error({
84
+ err: error,
85
+ collection,
86
+ documentId,
87
+ targetLocale: record.targetLocale,
88
+ sourceLocale: record.sourceLocale,
89
+ msg: "translator: failed to compute staleness for locale"
90
+ });
91
+ }
92
+ }
93
+ return locales;
94
+ }
95
+ /**
96
+ * Acknowledge the current source drift for one target locale: persist the current fingerprint as the
97
+ * dismissed one, so the indicator hides until the source changes again. No-op when the collection has
98
+ * no schema or the locale has no record.
99
+ */ async dismiss(key) {
100
+ const schema = this.schemaMap.get(key.collectionSlug);
101
+ if (!schema) return;
102
+ const record = await this.store.find(key);
103
+ if (!record) return;
104
+ const currentFingerprint = this.makeCurrentFingerprint(key.collectionSlug, key.documentId, schema);
105
+ await this.store.dismiss(key, await currentFingerprint(record.sourceLocale));
106
+ }
107
+ /**
108
+ * Recompute the current source fingerprint the same way the write path does (shared fetch shape +
109
+ * hash). Cached per source locale so a document translated from one source into N locales fetches
110
+ * the source once.
111
+ */ makeCurrentFingerprint(collection, documentId, schema) {
112
+ const cache = new Map();
113
+ return async (sourceLocale)=>{
114
+ const cached = cache.get(sourceLocale);
115
+ if (cached !== undefined) return cached;
116
+ const sourceData = await fetchSourceDocument(this.payload, collection, documentId, sourceLocale);
117
+ const fingerprint = computeSourceFingerprint(sourceData, schema);
118
+ cache.set(sourceLocale, fingerprint);
119
+ return fingerprint;
120
+ };
121
+ }
122
+ }
123
+
124
+ //# sourceMappingURL=Provenance.service.js.map
@@ -0,0 +1,26 @@
1
+ import type { CollectionAfterDeleteHook } from "payload";
2
+ /**
3
+ * The minimal slice of a Payload collection that provenance's config-time wiring reads and mutates:
4
+ * its `slug`, the sidecar `custom` marker, and the `afterDelete` hook slot. A real `CollectionConfig`
5
+ * is **structurally assignable** to this — call sites pass the live collection with no adapter, and a
6
+ * test passes a plain `{ slug: "posts" }` literal. Keeps `injectProvenanceCleanup` /
7
+ * `ensureProvenanceCollectionRegistered` off the god-`Config`/`CollectionConfig` types.
8
+ *
9
+ * The only Payload type imported here is `CollectionAfterDeleteHook` — a framework callback contract
10
+ * that legitimately stays framework-typed.
11
+ */
12
+ export type ManagedCollectionEntry = {
13
+ slug: string;
14
+ custom?: unknown;
15
+ hooks?: {
16
+ afterDelete?: CollectionAfterDeleteHook[];
17
+ };
18
+ };
19
+ /**
20
+ * The minimal config host provenance's config-time wiring touches: just a mutable `collections`
21
+ * array. A real Payload `Config` plugs straight in (its `collections?: CollectionConfig[]` satisfies
22
+ * `ManagedCollectionEntry[]`).
23
+ */
24
+ export type ManagedCollectionsConfig = {
25
+ collections?: ManagedCollectionEntry[];
26
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The minimal config host provenance's config-time wiring touches: just a mutable `collections`
3
+ * array. A real Payload `Config` plugs straight in (its `collections?: CollectionConfig[]` satisfies
4
+ * `ManagedCollectionEntry[]`).
5
+ */ export { };
6
+
7
+ //# sourceMappingURL=Provenance.shapes.js.map
@@ -129,4 +129,4 @@ function toRecord(doc) {
129
129
  }
130
130
  }
131
131
 
132
- //# sourceMappingURL=PayloadProvenanceStore.js.map
132
+ //# sourceMappingURL=Provenance.store.js.map
@@ -0,0 +1,23 @@
1
+ import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
2
+ import type { ConfigModifier } from "../../../types/ConfigModifier";
3
+ import type { ProvenanceServiceFactory } from "./Provenance.service";
4
+ /** The opt-in `provenance` plugin option (kept local so this module doesn't depend on plugin.ts). */
5
+ export type ProvenanceOption = boolean | {
6
+ slug?: string;
7
+ } | undefined;
8
+ /**
9
+ * Everything the provenance module contributes at config time, in one object (mirrors
10
+ * `TaskRunnerProvider.configure`): the request-scoped {@link ProvenanceService} factory used by the
11
+ * handlers/routes, and the single {@link ConfigModifier} that registers the sidecar collection and
12
+ * the cleanup hook. When provenance is disabled, `serviceFactory` is absent and `configure` is a no-op.
13
+ */
14
+ export type ProvenanceModule = {
15
+ serviceFactory?: ProvenanceServiceFactory;
16
+ configure(managedSlugs: Set<string>): ConfigModifier;
17
+ };
18
+ /**
19
+ * Turn the opt-in `provenance` option into a self-contained {@link ProvenanceModule}. This is the one
20
+ * place provenance's config-time wiring lives — `plugin.ts` only calls `configureProvenance(...)` and
21
+ * registers the returned modifier through the shared builder (no raw `config.collections` mutation).
22
+ */
23
+ export declare function configureProvenance(option: ProvenanceOption, schemaMap: CollectionSchemaMap): ProvenanceModule;
@@ -0,0 +1,43 @@
1
+ import { ProvenanceService } from "./Provenance.service";
2
+ import { PayloadProvenanceStore } from "./Provenance.store";
3
+ import { DEFAULT_PROVENANCE_SLUG, ensureProvenanceCollectionRegistered, isProvenanceCollection } from "./Provenance.collection";
4
+ import { injectProvenanceCleanup } from "./ProvenanceCleanup.hook";
5
+ import { assertProvenanceSlugFree } from "./slugGuard";
6
+ /**
7
+ * Resolve the opt-in `provenance` config to a sidecar slug, or `null` when disabled.
8
+ * `false`/omitted → off; `true` or `{}` → on with the default slug; `{ slug }` → on with that slug.
9
+ */ function resolveProvenanceSlug(option) {
10
+ if (!option) return null;
11
+ if (option === true) return DEFAULT_PROVENANCE_SLUG;
12
+ // `||` (not `??`) so an empty/blank slug falls back to the default instead of silently disabling.
13
+ return option.slug || DEFAULT_PROVENANCE_SLUG;
14
+ }
15
+ const NOOP = (config)=>config;
16
+ /**
17
+ * Turn the opt-in `provenance` option into a self-contained {@link ProvenanceModule}. This is the one
18
+ * place provenance's config-time wiring lives — `plugin.ts` only calls `configureProvenance(...)` and
19
+ * registers the returned modifier through the shared builder (no raw `config.collections` mutation).
20
+ */ export function configureProvenance(option, schemaMap) {
21
+ const slug = resolveProvenanceSlug(option);
22
+ if (!slug) return {
23
+ configure: ()=>NOOP
24
+ };
25
+ const storeFactory = (payload)=>new PayloadProvenanceStore(payload, slug);
26
+ const serviceFactory = (payload)=>new ProvenanceService(payload, storeFactory(payload), schemaMap);
27
+ const configure = (managedSlugs)=>(config)=>{
28
+ // `config` infers as Payload's `Config` from the ConfigModifier return type, so this leaf never
29
+ // names the god-type — it only reads/mutates through narrow helpers below.
30
+ // Fail fast on a slug collision with a consumer collection (ignoring our own sidecar on a repeat
31
+ // init, so an idempotent re-run doesn't false-positive).
32
+ assertProvenanceSlugFree(slug, (config.collections ?? []).filter((collection)=>!isProvenanceCollection(collection)));
33
+ ensureProvenanceCollectionRegistered(config, slug);
34
+ injectProvenanceCleanup(config, managedSlugs, storeFactory, slug);
35
+ return config;
36
+ };
37
+ return {
38
+ serviceFactory,
39
+ configure
40
+ };
41
+ }
42
+
43
+ //# sourceMappingURL=Provenance.wiring.js.map
@@ -1,5 +1,6 @@
1
- import type { CollectionAfterDeleteHook, Config } from "payload";
2
- import type { ProvenanceStoreFactory } from "./PayloadProvenanceStore";
1
+ import type { CollectionAfterDeleteHook } from "payload";
2
+ import type { ManagedCollectionsConfig } from "./Provenance.shapes";
3
+ import type { ProvenanceStoreFactory } from "./Provenance.store";
3
4
  /**
4
5
  * Build the `afterDelete` hook that cascade-deletes a document's provenance rows for `provenanceSlug`.
5
6
  *
@@ -15,4 +16,4 @@ export declare function makeProvenanceCleanupHook(storeFactory: ProvenanceStoreF
15
16
  * yet a second instance with a different slug still attaches its own hook. The sidecar collection is
16
17
  * never in `managedSlugs`, so it is never hooked (no recursion).
17
18
  */
18
- export declare function injectProvenanceCleanup(config: Config, managedSlugs: Set<string>, storeFactory: ProvenanceStoreFactory, provenanceSlug: string): void;
19
+ export declare function injectProvenanceCleanup(config: ManagedCollectionsConfig, managedSlugs: Set<string>, storeFactory: ProvenanceStoreFactory, provenanceSlug: string): void;
@@ -38,4 +38,4 @@
38
38
  }
39
39
  }
40
40
 
41
- //# sourceMappingURL=provenanceCleanupHook.js.map
41
+ //# sourceMappingURL=ProvenanceCleanup.hook.js.map
@@ -1,5 +1,9 @@
1
- export { DEFAULT_PROVENANCE_SLUG, isProvenanceCollection, makeProvenanceCollection, } from "./provenanceCollection";
2
- export { PayloadProvenanceStore } from "./PayloadProvenanceStore";
3
- export type { ProvenanceStoreFactory } from "./PayloadProvenanceStore";
4
- export { injectProvenanceCleanup, makeProvenanceCleanupHook } from "./provenanceCleanupHook";
1
+ export { DEFAULT_PROVENANCE_SLUG, isProvenanceCollection, makeProvenanceCollection, } from "./Provenance.collection";
2
+ export { PayloadProvenanceStore } from "./Provenance.store";
3
+ export type { ProvenanceStoreFactory } from "./Provenance.store";
4
+ export { injectProvenanceCleanup, makeProvenanceCleanupHook } from "./ProvenanceCleanup.hook";
5
5
  export { assertProvenanceSlugFree } from "./slugGuard";
6
+ export { ProvenanceService } from "./Provenance.service";
7
+ export type { ProvenanceServiceFactory, StalenessLocale } from "./Provenance.service";
8
+ export { configureProvenance } from "./Provenance.wiring";
9
+ export type { ProvenanceModule, ProvenanceOption } from "./Provenance.wiring";
@@ -1,8 +1,10 @@
1
1
  // Provenance adapter (Payload-backed). The framework-agnostic port + record types live in the core
2
- // (src/core/provenance); this module is the plugin-side implementation.
3
- export { DEFAULT_PROVENANCE_SLUG, isProvenanceCollection, makeProvenanceCollection } from "./provenanceCollection";
4
- export { PayloadProvenanceStore } from "./PayloadProvenanceStore";
5
- export { injectProvenanceCleanup, makeProvenanceCleanupHook } from "./provenanceCleanupHook";
2
+ // (src/core/provenance); this module is the plugin-side implementation + its config-time wiring.
3
+ export { DEFAULT_PROVENANCE_SLUG, isProvenanceCollection, makeProvenanceCollection } from "./Provenance.collection";
4
+ export { PayloadProvenanceStore } from "./Provenance.store";
5
+ export { injectProvenanceCleanup, makeProvenanceCleanupHook } from "./ProvenanceCleanup.hook";
6
6
  export { assertProvenanceSlugFree } from "./slugGuard";
7
+ export { ProvenanceService } from "./Provenance.service";
8
+ export { configureProvenance } from "./Provenance.wiring";
7
9
 
8
10
  //# sourceMappingURL=index.js.map
@@ -2,20 +2,13 @@ import type { CollectionConfig, Config, Endpoint } from "payload";
2
2
  import type { AccessGuard } from "../../../types/AccessGuard";
3
3
  import type { RawPayloadComponentExport } from "../../../types/PayloadComponentExport";
4
4
  import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
5
+ import type { ConfigModifier } from "../../../types/ConfigModifier";
5
6
  import type { TranslationProvider } from "../../../core/translation-providers";
6
7
  import type { TaskRunnerFactory } from "../task-runner";
7
- import type { ProvenanceStoreFactory } from "../provenance";
8
- import type { CollectionAdminSlot, LevelContext } from "./types";
9
- type ConfigModifier = (config: Config) => Config;
10
- export type PluginConfigBuilderDeps = {
11
- collections: CollectionConfig[];
12
- basePath: string;
13
- access?: AccessGuard;
14
- taskRunnerFactory: TaskRunnerFactory;
15
- schemaMap: CollectionSchemaMap;
16
- translationProvider: TranslationProvider;
17
- provenanceStoreFactory?: ProvenanceStoreFactory;
18
- };
8
+ import type { ProvenanceServiceFactory } from "../provenance";
9
+ import type { CollectionAdminSlot, LevelContext, TranslationContext } from "./types";
10
+ /** The builder's construction deps are exactly the shared {@link TranslationContext}. */
11
+ export type PluginConfigBuilderDeps = TranslationContext;
19
12
  /**
20
13
  * The single place that mutates the Payload `config`. Levels (through the narrow
21
14
  * {@link LevelContext}) and the plugin (through `addAdminProvider` /
@@ -34,7 +27,7 @@ export declare class PluginConfigBuilder implements LevelContext {
34
27
  readonly taskRunnerFactory: TaskRunnerFactory;
35
28
  readonly schemaMap: CollectionSchemaMap;
36
29
  readonly translationProvider: TranslationProvider;
37
- readonly provenanceStoreFactory?: ProvenanceStoreFactory;
30
+ readonly provenanceServiceFactory?: ProvenanceServiceFactory;
38
31
  private readonly endpoints;
39
32
  private readonly collectionComponents;
40
33
  private readonly adminProviders;
@@ -52,4 +45,3 @@ export declare class PluginConfigBuilder implements LevelContext {
52
45
  private attachCollectionComponents;
53
46
  private registerEndpoints;
54
47
  }
55
- export {};
@@ -34,7 +34,7 @@ function attachToSlot(collection, slot, component) {
34
34
  taskRunnerFactory;
35
35
  schemaMap;
36
36
  translationProvider;
37
- provenanceStoreFactory;
37
+ provenanceServiceFactory;
38
38
  endpoints = [];
39
39
  collectionComponents = [];
40
40
  adminProviders = [];
@@ -46,7 +46,7 @@ function attachToSlot(collection, slot, component) {
46
46
  this.taskRunnerFactory = deps.taskRunnerFactory;
47
47
  this.schemaMap = deps.schemaMap;
48
48
  this.translationProvider = deps.translationProvider;
49
- this.provenanceStoreFactory = deps.provenanceStoreFactory;
49
+ this.provenanceServiceFactory = deps.provenanceServiceFactory;
50
50
  }
51
51
  addEndpoints(endpoints) {
52
52
  this.endpoints.push(...endpoints);
@@ -1 +1 @@
1
- export type { TranslationLevel, LevelContext, CollectionAdminSlot } from "./types";
1
+ export type { TranslationLevel, LevelContext, TranslationContext, CollectionAdminSlot, } from "./types";
@@ -4,8 +4,26 @@ import type { RawPayloadComponentExport } from "../../../types/PayloadComponentE
4
4
  import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
5
5
  import type { TranslationProvider } from "../../../core/translation-providers";
6
6
  import type { TaskRunnerFactory } from "../task-runner";
7
- import type { ProvenanceStoreFactory } from "../provenance";
7
+ import type { ProvenanceServiceFactory } from "../provenance";
8
8
  export type CollectionAdminSlot = "beforeDocumentControls" | "beforeListTable";
9
+ /**
10
+ * The one bundle of config-time dependencies every translation surface / route / handler reads —
11
+ * the single source of truth that {@link LevelContext}, `PluginConfigBuilderDeps`, `StalenessConfig`
12
+ * and `TranslationRoutesDeps` all derive from (via `extends`/`Pick`), so a field can never drift
13
+ * between them.
14
+ */
15
+ export type TranslationContext = {
16
+ readonly collections: CollectionConfig[];
17
+ readonly basePath: string;
18
+ readonly access?: AccessGuard;
19
+ readonly taskRunnerFactory: TaskRunnerFactory;
20
+ /** Deep-cloned localized field schema per managed collection slug. */
21
+ readonly schemaMap: CollectionSchemaMap;
22
+ /** The configured translation backend (used by the synchronous field level). */
23
+ readonly translationProvider: TranslationProvider;
24
+ /** Builds a provenance service; absent when provenance is disabled (staleness then reports empty). */
25
+ readonly provenanceServiceFactory?: ProvenanceServiceFactory;
26
+ };
9
27
  /**
10
28
  * A composable translation surface (document / collection / field).
11
29
  *
@@ -32,17 +50,7 @@ export interface TranslationLevel {
32
50
  * Payload config. The plugin deduplicates endpoints by method + path on apply.
33
51
  * @internal
34
52
  */
35
- export interface LevelContext {
36
- readonly collections: CollectionConfig[];
37
- readonly basePath: string;
38
- readonly access?: AccessGuard;
39
- readonly taskRunnerFactory: TaskRunnerFactory;
40
- /** Deep-cloned localized field schema per managed collection slug. */
41
- readonly schemaMap: CollectionSchemaMap;
42
- /** The configured translation backend (used by the synchronous field level). */
43
- readonly translationProvider: TranslationProvider;
44
- /** Builds a provenance store; absent when provenance is disabled (staleness then reports empty). */
45
- readonly provenanceStoreFactory?: ProvenanceStoreFactory;
53
+ export interface LevelContext extends TranslationContext {
46
54
  /** Register endpoints. Deduplicated by method + path when applied. */
47
55
  addEndpoints(endpoints: Endpoint[]): void;
48
56
  /** Attach an admin component to a slot on every managed collection. */
@@ -0,0 +1,8 @@
1
+ import type { CollectionSlug, Payload } from "payload";
2
+ /**
3
+ * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
4
+ * (`depth: 0`, the given locale). Used by the translation **write** path (fingerprints the source it
5
+ * just translated) and, through {@link ProvenanceService}, by the staleness **read** path
6
+ * (re-fingerprints the live source), so the fingerprint baseline can never drift between the two.
7
+ */
8
+ export declare function fetchSourceDocument(payload: Payload, collection: CollectionSlug, id: string, locale: string): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
3
+ * (`depth: 0`, the given locale). Used by the translation **write** path (fingerprints the source it
4
+ * just translated) and, through {@link ProvenanceService}, by the staleness **read** path
5
+ * (re-fingerprints the live source), so the fingerprint baseline can never drift between the two.
6
+ */ export function fetchSourceDocument(payload, collection, id, locale) {
7
+ return payload.findByID({
8
+ collection,
9
+ id,
10
+ locale,
11
+ depth: 0
12
+ });
13
+ }
14
+
15
+ //# sourceMappingURL=sourceDocument.js.map
@@ -0,0 +1,10 @@
1
+ import type { Config } from "payload";
2
+ /**
3
+ * A config-time contribution: takes the Payload `config` and returns it (possibly a fresh object).
4
+ * Each translator module exposes a `configure(ctx) → ConfigModifier`; the plugin registers them
5
+ * through `PluginConfigBuilder.addConfigModifier`, which is the one place they are applied.
6
+ *
7
+ * Lives in `types/` (a leaf contract layer) so both `translation-levels` and the `provenance`
8
+ * module can import it without creating a module cycle.
9
+ */
10
+ export type ConfigModifier = (config: Config) => Config;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A config-time contribution: takes the Payload `config` and returns it (possibly a fresh object).
3
+ * Each translator module exposes a `configure(ctx) → ConfigModifier`; the plugin registers them
4
+ * through `PluginConfigBuilder.addConfigModifier`, which is the one place they are applied.
5
+ *
6
+ * Lives in `types/` (a leaf contract layer) so both `translation-levels` and the `provenance`
7
+ * module can import it without creating a module cycle.
8
+ */ export { };
9
+
10
+ //# sourceMappingURL=ConfigModifier.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,9 +0,0 @@
1
- import type { CollectionSlug, Payload } from "payload";
2
- /**
3
- * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
4
- * (`depth: 0`, the given locale). Shared by the translation **write** path (which fingerprints the
5
- * source it just translated) and the staleness **read** path (which re-fingerprints the live source),
6
- * so the fingerprint baseline can never drift between the two — the single biggest correctness trap in
7
- * staleness detection is thus enforced structurally, not by a comment.
8
- */
9
- export declare function fetchSourceDocument(payload: Payload, collection: CollectionSlug, id: string, locale: string): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
@@ -1,16 +0,0 @@
1
- /**
2
- * Fetch a document's source-locale data with the exact shape the provenance fingerprint depends on
3
- * (`depth: 0`, the given locale). Shared by the translation **write** path (which fingerprints the
4
- * source it just translated) and the staleness **read** path (which re-fingerprints the live source),
5
- * so the fingerprint baseline can never drift between the two — the single biggest correctness trap in
6
- * staleness detection is thus enforced structurally, not by a comment.
7
- */ export function fetchSourceDocument(payload, collection, id, locale) {
8
- return payload.findByID({
9
- collection,
10
- id,
11
- locale,
12
- depth: 0
13
- });
14
- }
15
-
16
- //# sourceMappingURL=sourceDocument.js.map
@@ -1,15 +0,0 @@
1
- import type { CollectionSlug, Payload } from "payload";
2
- import type { StalenessConfig, StalenessLocaleOutput } from "./model";
3
- /**
4
- * Per-locale staleness for one document: read every provenance record, recompute the current source
5
- * fingerprint, and mark each locale stale when the source drifted and the drift was not dismissed.
6
- * Returns `[]` when provenance is disabled or the collection has no schema — a normal condition, not
7
- * an error (genuine store/fetch failures propagate for the caller to log).
8
- */
9
- export declare function computeDocumentStaleness(payload: Payload, config: StalenessConfig, collection: CollectionSlug, documentId: string): Promise<StalenessLocaleOutput[]>;
10
- /**
11
- * Acknowledge the current source drift for one target locale: recompute the current source
12
- * fingerprint (write-path-identical) and persist it as the dismissed fingerprint, so the indicator
13
- * hides until the source changes again. No-op when provenance is disabled or the locale has no record.
14
- */
15
- export declare function dismissLocaleStaleness(payload: Payload, config: StalenessConfig, collection: CollectionSlug, documentId: string, targetLocale: string): Promise<void>;
@@ -1,78 +0,0 @@
1
- import { computeSourceFingerprint } from "../../../core/content-projection/computeSourceFingerprint";
2
- import { isRecordStale } from "../../../core/provenance";
3
- import { fetchSourceDocument } from "../_lib/sourceDocument";
4
- /**
5
- * Recompute the current source fingerprint the **same way the write path does**. Both sides fetch via
6
- * the shared {@link fetchSourceDocument} (identical `depth`/locale shape) and hash with the shared
7
- * {@link computeSourceFingerprint} + the same `schemaMap` entry, so an untouched document can never
8
- * report stale. Cached per source locale so a document translated from one source into N locales
9
- * fetches the source once.
10
- */ function makeCurrentFingerprint(payload, collection, documentId, schema) {
11
- const cache = new Map();
12
- return async (sourceLocale)=>{
13
- const cached = cache.get(sourceLocale);
14
- if (cached !== undefined) return cached;
15
- const sourceData = await fetchSourceDocument(payload, collection, documentId, sourceLocale);
16
- const fingerprint = computeSourceFingerprint(sourceData, schema);
17
- cache.set(sourceLocale, fingerprint);
18
- return fingerprint;
19
- };
20
- }
21
- /**
22
- * Per-locale staleness for one document: read every provenance record, recompute the current source
23
- * fingerprint, and mark each locale stale when the source drifted and the drift was not dismissed.
24
- * Returns `[]` when provenance is disabled or the collection has no schema — a normal condition, not
25
- * an error (genuine store/fetch failures propagate for the caller to log).
26
- */ export async function computeDocumentStaleness(payload, config, collection, documentId) {
27
- const schema = config.schemaMap.get(collection);
28
- if (!config.provenanceStoreFactory || !schema) return [];
29
- const store = config.provenanceStoreFactory(payload);
30
- const records = await store.findByDocument(collection, documentId);
31
- if (records.length === 0) return [];
32
- const currentFingerprint = makeCurrentFingerprint(payload, collection, documentId, schema);
33
- const locales = [];
34
- for (const record of records){
35
- try {
36
- const current = await currentFingerprint(record.sourceLocale);
37
- locales.push({
38
- target_lng: record.targetLocale,
39
- source_lng: record.sourceLocale,
40
- is_stale: isRecordStale(record, current),
41
- translated_at: record.translatedAt
42
- });
43
- } catch (error) {
44
- // Isolate per-locale failures (e.g. the record's source locale was removed from Payload's
45
- // localization config): skip just this locale so one bad record can't blank the staleness of
46
- // every other, still-valid locale on the document.
47
- payload.logger.error({
48
- err: error,
49
- collection,
50
- documentId,
51
- targetLocale: record.targetLocale,
52
- sourceLocale: record.sourceLocale,
53
- msg: "translator: failed to compute staleness for locale"
54
- });
55
- }
56
- }
57
- return locales;
58
- }
59
- /**
60
- * Acknowledge the current source drift for one target locale: recompute the current source
61
- * fingerprint (write-path-identical) and persist it as the dismissed fingerprint, so the indicator
62
- * hides until the source changes again. No-op when provenance is disabled or the locale has no record.
63
- */ export async function dismissLocaleStaleness(payload, config, collection, documentId, targetLocale) {
64
- const schema = config.schemaMap.get(collection);
65
- if (!config.provenanceStoreFactory || !schema) return;
66
- const store = config.provenanceStoreFactory(payload);
67
- const key = {
68
- collectionSlug: collection,
69
- documentId,
70
- targetLocale
71
- };
72
- const record = await store.find(key);
73
- if (!record) return;
74
- const currentFingerprint = makeCurrentFingerprint(payload, collection, documentId, schema);
75
- await store.dismiss(key, await currentFingerprint(record.sourceLocale));
76
- }
77
-
78
- //# sourceMappingURL=service.js.map