@orthacms/content-server 0.4.3 → 0.5.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.
Files changed (144) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -1
  4. package/dist/lib/content.module.d.ts.map +1 -1
  5. package/dist/lib/content.module.js +8 -0
  6. package/dist/lib/copilot/entry-proposal.applier.d.ts.map +1 -1
  7. package/dist/lib/copilot/entry-proposal.applier.js +5 -4
  8. package/dist/lib/docs/describe-content-api.d.ts +7 -0
  9. package/dist/lib/docs/describe-content-api.d.ts.map +1 -1
  10. package/dist/lib/docs/describe-content-api.js +177 -53
  11. package/dist/lib/docs/describe-views-api.d.ts +19 -0
  12. package/dist/lib/docs/describe-views-api.d.ts.map +1 -0
  13. package/dist/lib/docs/describe-views-api.js +76 -0
  14. package/dist/lib/docs/field-schema.d.ts.map +1 -1
  15. package/dist/lib/docs/field-schema.js +36 -2
  16. package/dist/lib/docs/openapi-writer.d.ts +49 -0
  17. package/dist/lib/docs/openapi-writer.d.ts.map +1 -0
  18. package/dist/lib/docs/openapi-writer.js +76 -0
  19. package/dist/lib/docs/public-api-schemas.d.ts +45 -0
  20. package/dist/lib/docs/public-api-schemas.d.ts.map +1 -0
  21. package/dist/lib/docs/public-api-schemas.js +377 -0
  22. package/dist/lib/docs/views-schemas.d.ts +22 -0
  23. package/dist/lib/docs/views-schemas.d.ts.map +1 -0
  24. package/dist/lib/docs/views-schemas.js +156 -0
  25. package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.d.ts +8 -2
  26. package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.d.ts.map +1 -1
  27. package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.js +19 -4
  28. package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.d.ts +2 -2
  29. package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.d.ts.map +1 -1
  30. package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.js +14 -4
  31. package/dist/lib/entries/application/use-cases/publish-entry.use-case.d.ts +6 -8
  32. package/dist/lib/entries/application/use-cases/publish-entry.use-case.d.ts.map +1 -1
  33. package/dist/lib/entries/application/use-cases/publish-entry.use-case.js +10 -4
  34. package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.d.ts +2 -5
  35. package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.d.ts.map +1 -1
  36. package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.js +6 -1
  37. package/dist/lib/entries/domain/entry.d.ts +8 -1
  38. package/dist/lib/entries/domain/entry.d.ts.map +1 -1
  39. package/dist/lib/entries/domain/entry.js +12 -12
  40. package/dist/lib/entries/domain/events/entry-events.d.ts +47 -5
  41. package/dist/lib/entries/domain/events/entry-events.d.ts.map +1 -1
  42. package/dist/lib/entries/domain/events/entry-events.js +26 -10
  43. package/dist/lib/entries/entries.constants.d.ts +10 -2
  44. package/dist/lib/entries/entries.constants.d.ts.map +1 -1
  45. package/dist/lib/entries/entries.constants.js +11 -2
  46. package/dist/lib/entries/http/controllers/bulk-entries.controller.d.ts +2 -2
  47. package/dist/lib/entries/http/controllers/bulk-entries.controller.d.ts.map +1 -1
  48. package/dist/lib/entries/http/controllers/bulk-entries.controller.js +8 -6
  49. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts +22 -6
  50. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
  51. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +71 -17
  52. package/dist/lib/insights/docs/describe-content-insights-api.d.ts +23 -0
  53. package/dist/lib/insights/docs/describe-content-insights-api.d.ts.map +1 -0
  54. package/dist/lib/insights/docs/describe-content-insights-api.js +329 -0
  55. package/dist/lib/mcp/content-tools.provider.d.ts.map +1 -1
  56. package/dist/lib/mcp/content-tools.provider.js +10 -9
  57. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts +14 -13
  58. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts.map +1 -1
  59. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.js +62 -46
  60. package/dist/lib/public-api/http/guards/api-token-workspace.guard.d.ts.map +1 -1
  61. package/dist/lib/public-api/http/guards/api-token-workspace.guard.js +8 -0
  62. package/dist/lib/public-api/infrastructure/public-entry-writes.service.d.ts +10 -9
  63. package/dist/lib/public-api/infrastructure/public-entry-writes.service.d.ts.map +1 -1
  64. package/dist/lib/public-api/infrastructure/public-entry-writes.service.js +23 -24
  65. package/dist/lib/public-api/infrastructure/token-actor.d.ts +37 -0
  66. package/dist/lib/public-api/infrastructure/token-actor.d.ts.map +1 -0
  67. package/dist/lib/public-api/infrastructure/token-actor.js +58 -0
  68. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.d.ts.map +1 -1
  69. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.js +12 -1
  70. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.d.ts +28 -0
  71. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.d.ts.map +1 -0
  72. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.js +52 -0
  73. package/dist/lib/utils/content-plugin.d.ts.map +1 -1
  74. package/dist/lib/utils/content-plugin.js +10 -1
  75. package/dist/lib/utils/content-views-plugin.d.ts.map +1 -1
  76. package/dist/lib/utils/content-views-plugin.js +8 -0
  77. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts +4 -1
  78. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts.map +1 -1
  79. package/dist/lib/views/application/use-cases/create-saved-view.use-case.js +33 -9
  80. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +4 -1
  81. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts.map +1 -1
  82. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.js +24 -3
  83. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts +4 -1
  84. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts.map +1 -1
  85. package/dist/lib/views/application/use-cases/update-saved-view.use-case.js +32 -3
  86. package/dist/lib/views/domain/events/saved-view-events.d.ts +28 -0
  87. package/dist/lib/views/domain/events/saved-view-events.d.ts.map +1 -0
  88. package/dist/lib/views/domain/events/saved-view-events.js +38 -0
  89. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts +12 -3
  90. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts.map +1 -1
  91. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.js +15 -5
  92. package/package.json +11 -11
  93. package/dist/lib/entries/controllers/bulk-entries.controller.d.ts +0 -26
  94. package/dist/lib/entries/controllers/bulk-entries.controller.d.ts.map +0 -1
  95. package/dist/lib/entries/controllers/create-entry.controller.d.ts +0 -20
  96. package/dist/lib/entries/controllers/create-entry.controller.d.ts.map +0 -1
  97. package/dist/lib/entries/controllers/delete-entry.controller.d.ts +0 -24
  98. package/dist/lib/entries/controllers/delete-entry.controller.d.ts.map +0 -1
  99. package/dist/lib/entries/controllers/get-entry.controller.d.ts +0 -34
  100. package/dist/lib/entries/controllers/get-entry.controller.d.ts.map +0 -1
  101. package/dist/lib/entries/controllers/list-entries.controller.d.ts +0 -20
  102. package/dist/lib/entries/controllers/list-entries.controller.d.ts.map +0 -1
  103. package/dist/lib/entries/controllers/publish-entry.controller.d.ts +0 -19
  104. package/dist/lib/entries/controllers/publish-entry.controller.d.ts.map +0 -1
  105. package/dist/lib/entries/controllers/resolve-type.d.ts +0 -9
  106. package/dist/lib/entries/controllers/resolve-type.d.ts.map +0 -1
  107. package/dist/lib/entries/controllers/update-entry.controller.d.ts +0 -18
  108. package/dist/lib/entries/controllers/update-entry.controller.d.ts.map +0 -1
  109. package/dist/lib/entries/dto/bulk-ids.dto.d.ts +0 -11
  110. package/dist/lib/entries/dto/bulk-ids.dto.d.ts.map +0 -1
  111. package/dist/lib/entries/dto/list-entries-query.dto.d.ts +0 -46
  112. package/dist/lib/entries/dto/list-entries-query.dto.d.ts.map +0 -1
  113. package/dist/lib/entries/dto/relation-delta-map.validator.d.ts +0 -19
  114. package/dist/lib/entries/dto/relation-delta-map.validator.d.ts.map +0 -1
  115. package/dist/lib/entries/dto/save-entry.dto.d.ts +0 -46
  116. package/dist/lib/entries/dto/save-entry.dto.d.ts.map +0 -1
  117. package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts +0 -24
  118. package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts.map +0 -1
  119. package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts +0 -40
  120. package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts.map +0 -1
  121. package/dist/lib/entries/services/entries.service.d.ts +0 -59
  122. package/dist/lib/entries/services/entries.service.d.ts.map +0 -1
  123. package/dist/lib/entries/services/entry-counter.service.d.ts +0 -31
  124. package/dist/lib/entries/services/entry-counter.service.d.ts.map +0 -1
  125. package/dist/lib/entries/services/entry-filter-schema.d.ts +0 -32
  126. package/dist/lib/entries/services/entry-filter-schema.d.ts.map +0 -1
  127. package/dist/lib/entries/services/entry-row.d.ts +0 -56
  128. package/dist/lib/entries/services/entry-row.d.ts.map +0 -1
  129. package/dist/lib/entries/services/entry-writer.service.d.ts +0 -193
  130. package/dist/lib/entries/services/entry-writer.service.d.ts.map +0 -1
  131. package/dist/lib/entries/services/relation-link.service.d.ts +0 -115
  132. package/dist/lib/entries/services/relation-link.service.d.ts.map +0 -1
  133. package/dist/lib/extension/entry-access-source.d.ts +0 -112
  134. package/dist/lib/extension/entry-access-source.d.ts.map +0 -1
  135. package/dist/lib/extension/entry-write-hook.d.ts +0 -98
  136. package/dist/lib/extension/entry-write-hook.d.ts.map +0 -1
  137. package/dist/lib/extension/per-locale-relation.d.ts +0 -18
  138. package/dist/lib/extension/per-locale-relation.d.ts.map +0 -1
  139. package/dist/lib/public/controllers/get-public-entry.controller.d.ts +0 -26
  140. package/dist/lib/public/controllers/get-public-entry.controller.d.ts.map +0 -1
  141. package/dist/lib/public/controllers/list-public-entries.controller.d.ts +0 -24
  142. package/dist/lib/public/controllers/list-public-entries.controller.d.ts.map +0 -1
  143. package/dist/lib/public/controllers/public-schema.controller.d.ts +0 -18
  144. package/dist/lib/public/controllers/public-schema.controller.d.ts.map +0 -1
@@ -1,56 +0,0 @@
1
- /**
2
- * Pure row ↔ entry mappers shared by the read ({@link EntriesService}) and write
3
- * ({@link EntryWriterService}) paths, so the wire shape and the column projection
4
- * have one definition each. No DB access, no NestJS — just shape translation.
5
- */
6
- import type { AnyContentType } from '../../types/content-type';
7
- import type { EntryRecord } from '../types/entry-list-view';
8
- /** A generated content table row seen as a bag of values by property name. */
9
- type Row = Record<string, unknown>;
10
- /**
11
- * Map a raw DB row to the admin {@link EntryRecord}: envelope fields plus a
12
- * `values` bag keyed by field name. `status` is emitted only for publishable
13
- * types. Relations that own no column — many-relations (their links live in join
14
- * tables) and inverse/back-references (they reuse the owning side's storage) —
15
- * aren't selected here; an owning single relation passes through as its FK uuid.
16
- */
17
- export declare function toRecord(type: AnyContentType, row: Row): EntryRecord;
18
- /**
19
- * Normalize the admin's (string-shaped) `values` bag to the proper JS types the
20
- * validator and storage expect — numbers parsed, JSON text parsed, empties
21
- * collapsed to null — keeping every field (so a many-relation array still
22
- * validates). The single coercion point: both {@link toColumns} (storage) and
23
- * the writer's validation run against the result, so they can't disagree on a
24
- * value's type. Datetime/date stay ISO strings (the validator accepts them;
25
- * {@link toColumns} converts datetime to `Date` for `timestamptz`).
26
- */
27
- export declare function coerceValues(type: AnyContentType, values: Record<string, unknown>): Record<string, unknown>;
28
- /**
29
- * Project a (coerced) `values` bag onto the columns of a generated table for an
30
- * insert/update. Includes only keys declared on the type (reserved envelope
31
- * columns — `status`/`published_at`/`deleted_at`/timestamps — are owned by the
32
- * service, never the client), skips relations that own no column (many-relations,
33
- * whose links live in join tables, and inverse/back-references, which reuse the
34
- * owning side's storage), collapses empties to `null`, converts a `datetime` to a `Date` for
35
- * the `timestamptz` column (guarding an invalid date to `null` rather than
36
- * letting `pg` throw), and drops a `NaN` number to `null`. A field absent from
37
- * `values` is written as `null` — the editor submits the full bag, so a write
38
- * replaces the whole document.
39
- */
40
- export declare function toColumns(type: AnyContentType, values: Record<string, unknown>): Record<string, unknown>;
41
- /**
42
- * A human display label for an entry — the first non-empty title-eligible field's
43
- * value (see {@link TITLE_FIELD_TYPES}), falling back to the id. Used to label
44
- * rows in the bulk-publish preview.
45
- */
46
- export declare function entryTitle(type: AnyContentType, row: Row): string;
47
- /**
48
- * The slug handle for an entry — the value of its **slug field**: the first
49
- * field flagged `admin.widget === 'slug'`, else a field literally named `slug`.
50
- * Returns the trimmed value when present and non-empty, otherwise `undefined`
51
- * (the type has no slug field, or the row's slug is blank). Used to give a
52
- * linked relation record a stable `/handle` in the admin.
53
- */
54
- export declare function entrySlug(type: AnyContentType, row: Row): string | undefined;
55
- export {};
56
- //# sourceMappingURL=entry-row.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry-row.d.ts","sourceRoot":"","sources":["../../../../src/lib/entries/services/entry-row.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,8EAA8E;AAC9E,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,WAAW,CAwBpE;AAKD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CACxB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACrB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB;AAWD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAOjE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAS5E"}
@@ -1,193 +0,0 @@
1
- import { type Database } from '@ortha-cms/database';
2
- import { type ContentEntryExtension } from '../../extension/entry-extension';
3
- import type { AnyContentType } from '../../types/content-type';
4
- import { EntryValidationService } from '../../validation/services/entry-validation.service';
5
- import type { EntryRecord, RelationDelta, RelationFieldView } from '../types/entry-list-view';
6
- import { type BulkActionResult, type BulkPublishPreview, type BulkPublishResult } from '../types/bulk-publish';
7
- import { RelationLinkService } from './relation-link.service';
8
- /**
9
- * The write half of the entries pipeline — create / read-one / update / publish
10
- * / unpublish / delete / restore / purge, plus their bulk variants. Generic over
11
- * the content type (like {@link EntriesService}): the physical table and its
12
- * envelope columns are derived from `type` at request time, so one service backs
13
- * every collection. {@link EntryValidationService} is the gate — nothing is
14
- * written or published without passing it.
15
- */
16
- export declare class EntryWriterService {
17
- private readonly db;
18
- private readonly validation;
19
- private readonly relations;
20
- private readonly extension?;
21
- constructor(db: Database, validation: EntryValidationService, relations: RelationLinkService, extension?: ContentEntryExtension | undefined);
22
- /**
23
- * Create an entry **owned by `workspaceId`** (stamped onto the row so the
24
- * reader's workspace filter and every later scoped write see it). A
25
- * publishable type starts as a **draft** and may be incomplete — validation
26
- * is deferred to publish. A non-publishable type is always live, so its
27
- * values must validate now.
28
- *
29
- * The insert runs in a transaction that first takes the workspace's *shared*
30
- * content lock ({@link lockWorkspaceShared}): it coordinates with the
31
- * workspace delete / content-revoke guards (which take the lock exclusively),
32
- * so a new entry can't land between their emptiness check and their mutation
33
- * and be orphaned. Shared mode keeps concurrent creates non-blocking.
34
- *
35
- * `locale` / `localeGroupId` are opaque, extension-owned params: the bound
36
- * extension validates them and stamps the envelope columns (a
37
- * `localeGroupId` makes the row a **sibling** in that translation group). A
38
- * duplicate `(group, locale)` surfaces as a **409** via {@link uniqueGuarded}.
39
- */
40
- create(type: AnyContentType, values: Record<string, unknown>, workspaceId: string, relations?: Record<string, RelationDelta>, locale?: string, localeGroupId?: string): Promise<EntryRecord>;
41
- /**
42
- * First page (+ total) of every relation field's links for one live entry,
43
- * keyed by field name — what the editor loads on open. Each field is
44
- * paginated, so a relation with many links contributes only its first page.
45
- * 404 if the entry is missing (or soft-deleted) in this workspace.
46
- */
47
- getRelations(type: AnyContentType, id: string, workspaceId: string): Promise<Record<string, RelationFieldView>>;
48
- /**
49
- * One page of a single relation field's links (infinite-scroll for a
50
- * many/inverse relation). 404 if the entry is missing; 400 if `field` isn't a
51
- * relation on the type.
52
- */
53
- getRelationField(type: AnyContentType, id: string, field: string, page: number, pageSize: number, workspaceId: string): Promise<RelationFieldView>;
54
- /**
55
- * Apply the staged per-field relation deltas of a save, inside the entry's
56
- * transaction — so the row and every link change commit or roll back as one.
57
- * Each field must be a **many/inverse** relation (a single relation is set
58
- * through `values`); a single-relation or unknown key is a 400. `applyDelta`
59
- * validates the linked ids against the workspace (422 on a bad target).
60
- */
61
- private applyRelationDeltas;
62
- /** Resolve a relation field spec on the type, or 400 if it isn't one. */
63
- private relationSpec;
64
- /** Read one live entry in the workspace, or 404. */
65
- getOne(type: AnyContentType, id: string, workspaceId: string): Promise<EntryRecord>;
66
- /**
67
- * Replace a live entry's values, or 404. As with {@link create}, a
68
- * publishable type's values aren't validated here (a draft may be saved
69
- * incomplete; publish enforces the rules) — a non-publishable, always-live
70
- * type validates now.
71
- */
72
- update(type: AnyContentType, id: string, values: Record<string, unknown>, workspaceId: string, relations?: Record<string, RelationDelta>): Promise<EntryRecord>;
73
- /** Validate the stored row, then mark it published (publishable types only). */
74
- publish(type: AnyContentType, id: string, workspaceId: string): Promise<EntryRecord>;
75
- /** Revert a live entry to draft (publishable types only). */
76
- unpublish(type: AnyContentType, id: string, workspaceId: string): Promise<EntryRecord>;
77
- /**
78
- * Delete an entry: soft (stamp `deleted_at`) for a paranoid type, hard
79
- * `DELETE` otherwise. 404 if there's no live row to remove **in this
80
- * workspace** — an id from another workspace is indistinguishable from a
81
- * missing one.
82
- */
83
- remove(type: AnyContentType, id: string, workspaceId: string): Promise<void>;
84
- /**
85
- * Clear a soft-deleted entry's tombstone (paranoid types only), or 404.
86
- * On an i18n type the restore can collide with a row created in the same
87
- * (locale group, locale) slot after the soft delete — the partial unique
88
- * index rejects it, surfaced as a 409 rather than a 500.
89
- */
90
- restore(type: AnyContentType, id: string, workspaceId: string): Promise<EntryRecord>;
91
- /** Permanently delete a tombstoned entry (paranoid types only), or 404. */
92
- purge(type: AnyContentType, id: string, workspaceId: string): Promise<void>;
93
- /**
94
- * Dry-run a publish over a set of ids: validate each live row and report a
95
- * verdict (will-publish / already-published / blocked / not-found) in
96
- * request order. Writes nothing.
97
- */
98
- previewBulkPublish(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkPublishPreview>;
99
- /**
100
- * Commit a bulk publish. The validation gate is **locked**: the candidate
101
- * rows are selected `FOR UPDATE` and re-validated inside one transaction, so
102
- * a concurrent edit can't invalidate a row between the dry run and the write
103
- * (the single-statement preview-then-update split had that TOCTOU window).
104
- * Publishes only the `publishable` ids, reporting which were skipped and why.
105
- */
106
- bulkPublish(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkPublishResult>;
107
- /**
108
- * Compute the per-entry publish verdict (will-publish / already-published /
109
- * blocked / not-found) for `ids` in request order, given the live rows keyed
110
- * by id. Pure — the dry run and the committed {@link bulkPublish} share it so
111
- * they can't disagree on what's publishable.
112
- */
113
- private verdictsFor;
114
- /**
115
- * The per-field publish-gate checklist for one record: every required field
116
- * plus any field that has an issue, each marked pass/fail. Mirrors the
117
- * editor's Publish Gate so a row can show its passed fields, not just the
118
- * failures.
119
- */
120
- private buildChecks;
121
- /** Revert a set of live entries to draft. */
122
- bulkUnpublish(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkActionResult>;
123
- /** Delete a set of entries: soft for paranoid types, hard otherwise. */
124
- bulkRemove(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkActionResult>;
125
- /** Permanently delete a set of tombstoned entries (paranoid types only). */
126
- bulkPurge(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkActionResult>;
127
- /**
128
- * Restore a set of soft-deleted entries (paranoid types only). As with
129
- * {@link restore}, a restored row colliding with a live sibling in its
130
- * (locale group, locale) slot is a 409 — the whole batch rolls back
131
- * (single statement), so the caller can retry without the conflicting id.
132
- */
133
- bulkRestore(type: AnyContentType, ids: string[], workspaceId: string): Promise<BulkActionResult>;
134
- /**
135
- * Run a write, translating a Postgres unique violation (23505) into a 409.
136
- * Only i18n types carry a restore-relevant unique index — the partial
137
- * `(locale_group_id, locale)` one — so the mapping is scoped to them and
138
- * any other type's violation still surfaces as the bug it is.
139
- */
140
- private uniqueGuarded;
141
- /** The type's generated table seen as a column bag (envelope + fields). */
142
- private columns;
143
- /**
144
- * `workspace_id = :workspaceId` — the ownership predicate every read and
145
- * write AND-s in, so a request scoped to one workspace can neither see nor
146
- * mutate another's rows.
147
- */
148
- private scope;
149
- /**
150
- * `id = :id` AND the workspace scope AND (for paranoid types)
151
- * `deleted_at IS NULL`.
152
- */
153
- private liveWhere;
154
- /** Fetch one live row by id in the workspace, or undefined. */
155
- private findLive;
156
- /** Fetch the workspace's live rows for a set of ids, keyed by id. */
157
- private loadLiveByIds;
158
- /**
159
- * Verify every referenced relation target exists **in the same workspace** —
160
- * single FKs, many-to-many links, and the inverse (back-reference) side of a
161
- * two-way relation. Neither the FK column nor the join table has a workspace
162
- * constraint of its own (the target table is workspace-scoped only at the app
163
- * layer), so without this a caller could reference — or probe the existence
164
- * of — an entry in another workspace. Batched one existence query per
165
- * referenced target type; a missing or cross-workspace target surfaces as a
166
- * uniform 422 validation issue (indistinguishable from a plain "invalid id",
167
- * so no not-found-vs-forbidden enumeration signal). Runs on every
168
- * create/update, draft or not, because links are written eagerly either way.
169
- * An inverse-of-single (one-to-many) field owns no writable link from this
170
- * side, so it's skipped.
171
- */
172
- private assertRelationTargets;
173
- /**
174
- * Enforce required **link-managed** relations (an owning many-to-many, or the
175
- * inverse of one) against the entry's actual link set — the check
176
- * {@link EntryValidationService} can't make, because those links never travel
177
- * in the `values` bag it sees. A required such relation with zero links is a
178
- * 422 `is required`, matching how a required scalar field reads. A single FK
179
- * relation is validated in `values` already; an inverse-of-single owns no
180
- * writable link from this side, so it's skipped (it can't be satisfied here).
181
- * `exec` is the write transaction on create/update (so it counts the
182
- * just-written, uncommitted rows) or the root client on publish (committed).
183
- */
184
- private assertRequiredRelations;
185
- /** Throw 422 with the issue list when the values fail validation. */
186
- private assertValid;
187
- /** Reject publish/unpublish on a type with no publish workflow. */
188
- private assertPublishable;
189
- /** Reject restore/purge on a type without soft delete. */
190
- private assertParanoid;
191
- private notFound;
192
- }
193
- //# sourceMappingURL=entry-writer.service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry-writer.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/entries/services/entry-writer.service.ts"],"names":[],"mappings":"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"}
@@ -1,115 +0,0 @@
1
- import { type Database } from '@ortha-cms/database';
2
- import type { AnyContentType } from '../../types/content-type';
3
- import { type AnyFieldSpec } from '../../types/fields';
4
- import type { RelationDelta, RelationFieldView } from '../types/entry-list-view';
5
- /** A generated content row seen as a bag of values by property name. */
6
- type Row = Record<string, unknown>;
7
- /** Default links per page when the caller doesn't specify one. */
8
- export declare const RELATION_PAGE_SIZE = 20;
9
- /**
10
- * The transaction handle Drizzle hands the `db.transaction(tx => …)` callback —
11
- * derived from {@link Database} so link writes share the exact query surface of
12
- * the client without hard-coding the dialect's transaction type.
13
- */
14
- export type DbTransaction = Parameters<Parameters<Database['transaction']>[0]>[0];
15
- /**
16
- * Reads and writes the relation **links** an entry owns beyond its own columns:
17
- * many-to-many join rows and the inverse (back-reference) side of a two-way
18
- * relation. Owning **single** relations are plain `<field>_id` FK columns —
19
- * written by `toColumns`, read off the row — so they need no join plumbing.
20
- *
21
- * Everything is **paginated**: a relation can hold thousands of links, so reads
22
- * return one ordered page + a `total` (never every id), and writes apply an
23
- * incremental {@link RelationDelta} (link / unlink / reorder) inside a
24
- * transaction — so a huge relation never has to be sent or held in full.
25
- * Order is persisted in the join table's `position` column, owned by the
26
- * **source** side; the inverse reads by it (stable) but can't reorder it.
27
- */
28
- export declare class RelationLinkService {
29
- private readonly db;
30
- constructor(db: Database);
31
- /**
32
- * First page (+ total) of **every** relation field of `type` for one entry,
33
- * keyed by field name — what the editor loads on open. Each field is read
34
- * independently (paginated), so a relation with many links contributes only
35
- * its first page, not every id.
36
- */
37
- readAll(type: AnyContentType, row: Row, workspaceId: string, pageSize?: number): Promise<Record<string, RelationFieldView>>;
38
- /**
39
- * One page of a single relation field's links, ordered by `position`
40
- * (owning) and resolved to display-ready refs (id + title). `row` is the
41
- * editing entry (its FK columns back a single relation); pass it to avoid a
42
- * re-read. Returns `{ items, total }`.
43
- */
44
- readField(type: AnyContentType, row: Row, field: string, spec: AnyFieldSpec, page: number, pageSize: number, workspaceId: string): Promise<RelationFieldView>;
45
- /**
46
- * Apply an incremental {@link RelationDelta} to one many/inverse relation
47
- * field inside `tx`: validate the linked targets are in the workspace, unlink
48
- * the removed pairs, append the new ones (`position = max+1` for the source,
49
- * `ON CONFLICT DO NOTHING`), then renumber to `order` (owning side only).
50
- * A single relation or an inverse-of-single owns no delta-writable link, so
51
- * it's a no-op. Returns nothing — read the field back for the new state.
52
- */
53
- applyDelta(tx: DbTransaction, type: AnyContentType, sourceId: string, field: string, delta: RelationDelta, workspaceId: string): Promise<void>;
54
- /**
55
- * Replace a many-relation's links with exactly `values[field]` (array),
56
- * position = array index — the **whole-set** write used only when a caller
57
- * genuinely submits a relation **array** in the entry body (legacy / bulk).
58
- * The editor uses {@link applyDelta} instead. Runs inside the entry's
59
- * transaction. **Only an array counts**: a field that's absent or `null`
60
- * (`coerceValues` stamps `null` onto every unset field) is left untouched, so
61
- * a save that manages a relation purely through deltas can never have its
62
- * links cleared here.
63
- */
64
- writeLinks(tx: DbTransaction, type: AnyContentType, sourceId: string, values: Record<string, unknown>, workspaceId: string): Promise<void>;
65
- /**
66
- * Count the links of one join-backed relation field for an entry — the
67
- * `total` a read would report, without fetching or resolving any page. Runs
68
- * on the passed executor (the write transaction when validating a just-saved
69
- * entry, so it sees the delta's uncommitted rows). Returns 0 for a relation
70
- * that owns no writable links from this side (single FK / inverse-of-single).
71
- */
72
- countLinks(exec: Database | DbTransaction, type: AnyContentType, row: Row, field: string, spec: AnyFieldSpec, workspaceId: string): Promise<number>;
73
- /**
74
- * Take the transaction-scoped advisory lock for one physical source list
75
- * (`<join table>:<sourceId>`), serializing every appender to that list so no
76
- * two concurrent writes read the same `max(position)`. Auto-releases at
77
- * commit/rollback. Keyed on the table name too, so distinct join tables that
78
- * happen to share a source id don't falsely contend.
79
- */
80
- private lockSource;
81
- /** Next append position for a source: `max(position) + 1`, else 0. */
82
- private nextPosition;
83
- /**
84
- * Resolve `ids` (in order) to display-ready refs via one lookup on the target
85
- * type, scoped to the workspace. Ids with no **live** row drop to an id-only
86
- * ref — a soft-deleted (paranoid) target is excluded here just as it is on
87
- * the inverse read side, so a trashed record never surfaces its title on the
88
- * owning side while it's hidden on the other.
89
- */
90
- private refsFor;
91
- /** Build a display ref from a full target row. */
92
- private rowToRef;
93
- /**
94
- * Verify every id to be linked exists **in the same workspace** (the join FK
95
- * has no workspace constraint of its own). A missing or cross-workspace id is
96
- * a uniform 422 — indistinguishable from an invalid id, so no enumeration
97
- * signal.
98
- */
99
- private assertTargets;
100
- /**
101
- * The join-table plan for a relation field, or `null` when it owns no join
102
- * table (an owning single relation, or an inverse-of-single). An owning
103
- * many-relation uses its own join table (`source→target`); an inverse of a
104
- * many-to-many reuses the owning side's join table with the roles swapped.
105
- */
106
- private joinPlanFor;
107
- /**
108
- * The inverse-of-single (one-to-many) plan for a relation field, or `null`:
109
- * it reads the owner rows whose FK points back at the editing record, and
110
- * owns no writable link from this side.
111
- */
112
- private inversePlanFor;
113
- }
114
- export {};
115
- //# sourceMappingURL=relation-link.service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"relation-link.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/entries/services/relation-link.service.ts"],"names":[],"mappings":"AAeA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,KAAK,EACR,aAAa,EACb,iBAAiB,EAEpB,MAAM,0BAA0B,CAAC;AAMlC,wEAAwE;AACxE,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAUrC;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAClC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CACzC,CAAC,CAAC,CAAC,CAAC;AA0BL;;;;;;;;;;;;GAYG;AACH,qBACa,mBAAmB;IACE,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAI3D;;;;;OAKG;IACG,OAAO,CACT,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,EACnB,QAAQ,SAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAoB7C;;;;;OAKG;IACG,SAAS,CACX,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,iBAAiB,CAAC;IA0E7B;;;;;;;OAOG;IACG,UAAU,CACZ,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,EACpB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC;IA+FhB;;;;;;;;;OASG;IACG,UAAU,CACZ,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC;IAwChB;;;;;;OAMG;IACG,UAAU,CACZ,IAAI,EAAE,QAAQ,GAAG,aAAa,EAC9B,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC;IA8BlB;;;;;;OAMG;YACW,UAAU;IAWxB,sEAAsE;YACxD,YAAY;IAc1B;;;;;;OAMG;YACW,OAAO;IAuBrB,kDAAkD;IAClD,OAAO,CAAC,QAAQ;IAWhB;;;;;OAKG;YACW,aAAa;IAmC3B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IA8BnB;;;;OAIG;IACH,OAAO,CAAC,cAAc;CAYzB"}
@@ -1,112 +0,0 @@
1
- /**
2
- * The **entry-access description port** — the seam a downstream plugin uses to
3
- * tell a protocol adapter *that* an entry it just served is restricted, without
4
- * this package knowing what a restriction is.
5
- *
6
- * It is the read scope's opposite number and deliberately not the same thing.
7
- * `CONTENT_READ_SCOPE` decides which rows a reader gets; this describes the rows
8
- * they got. A scope can only subtract, so by the time an annotation is asked
9
- * for, the entry has already been admitted — which is why nothing here can
10
- * refuse anything, and why an implementation that returned nothing would be a
11
- * missing label rather than a leak.
12
- *
13
- * ## What it is for
14
- *
15
- * One thing, and it is worth being blunt about it: **a restricted entry must not
16
- * be cached as public.** A CDN, a shared HTTP cache or a client store that keeps
17
- * one reader's copy and hands it to the next turns a working entitlement into a
18
- * data leak, and nothing else in the response says the entry was reader-scoped —
19
- * it looks exactly like an open one. Everything else an implementation could
20
- * report (who exactly may read it, which segments) is a question for the admin
21
- * API, where the caller is a member of the workspace rather than a consumer of
22
- * its content.
23
- *
24
- * ## Why a registry rather than a DI token
25
- *
26
- * The same reason the read scope uses one: Nest has **no multi-provider**, so
27
- * two dynamic modules binding one token do not merge — the second silently
28
- * replaces the first. Here that would mean an entry that one plugin knows is
29
- * restricted reported as open because a second plugin was registered after it,
30
- * which is precisely the mislabelling this port exists to prevent. Registration
31
- * is a runtime `register(...)` call, the same shape as
32
- * {@link contentReadScopeRegistrar}.
33
- *
34
- * ## The batching constraint
35
- *
36
- * `describe` takes the whole page of entry ids, not one. A protocol adapter
37
- * resolves a list and then labels it; asking per entry would be a query per row
38
- * on a path whose cost is otherwise one page read. An implementation that cannot
39
- * answer for an id simply leaves it out of the map, and the caller reads a
40
- * missing key as unrestricted — the same default absence carries everywhere
41
- * else in this feature.
42
- */
43
- import { type Provider, type Type } from '@nestjs/common';
44
- /** What one entry's restriction looks like from outside the plugin that owns it. */
45
- export interface EntryAccessDescription {
46
- /**
47
- * Whether anything narrowed who may read this entry.
48
- *
49
- * `true` means the response is reader-specific and must not be stored in a
50
- * shared cache without varying on whatever identifies the reader.
51
- */
52
- readonly restricted: boolean;
53
- /**
54
- * The axes that took part, as opaque keys (`org`, `plan`). A hint for a
55
- * client deciding what to vary its own cache on — never the segments
56
- * themselves, which would tell a consumer who *else* may read the entry.
57
- */
58
- readonly dimensions: readonly string[];
59
- }
60
- /** What a source is told about the page it is describing. */
61
- export interface EntryAccessQuery {
62
- /** The workspace the entries were read in. */
63
- readonly workspaceId: string;
64
- /** The ids to describe. Never empty — the caller skips an empty page. */
65
- readonly entryIds: readonly string[];
66
- }
67
- /**
68
- * One contributed description of what was served.
69
- *
70
- * Returning an empty map means "nothing to say about this page", which is what
71
- * an installed-but-unconfigured plugin returns and what every entry defaults to.
72
- */
73
- export interface EntryAccessSource {
74
- /** Describes the given entries, keyed by entry id. Ids may be omitted. */
75
- describe(query: EntryAccessQuery): Promise<ReadonlyMap<string, EntryAccessDescription>>;
76
- }
77
- /**
78
- * Every registered source, merged.
79
- *
80
- * The merge is a **union that can only tighten**: an entry is restricted if any
81
- * source says so, and the dimensions are the union of what each reported. Two
82
- * plugins narrowing reads for different reasons both need to be heard, and the
83
- * safe direction for a disagreement is the one that keeps a shared cache from
84
- * storing the entry.
85
- */
86
- export declare class EntryAccessSourceRegistry {
87
- private readonly sources;
88
- /** Adds a source. Registering the same instance twice is a no-op. */
89
- register(source: EntryAccessSource): void;
90
- /** Whether anything is registered — the cheap check before building a page. */
91
- get active(): boolean;
92
- /**
93
- * Describes one page of entries.
94
- *
95
- * Returns an empty map when nothing is registered or the page is empty, so
96
- * a caller on an installation with no scoping plugin does no work and every
97
- * entry reads as unrestricted.
98
- */
99
- describe(query: EntryAccessQuery): Promise<ReadonlyMap<string, EntryAccessDescription>>;
100
- }
101
- /**
102
- * Builds the DI provider that registers `sources` with content's access-source
103
- * registry at bootstrap.
104
- *
105
- * A factory with an explicit `inject` list rather than a class with reflected
106
- * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
107
- * dependency typed as `Foo | null` emits `Object` for `design:paramtypes`, Nest
108
- * injects `undefined` with no error, and the plugin ends up registering
109
- * nothing.
110
- */
111
- export declare function entryAccessSourceRegistrar(label: string, ...sources: Type<EntryAccessSource>[]): Provider;
112
- //# sourceMappingURL=entry-access-source.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry-access-source.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/entry-access-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AAExB,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,QAAQ,CACJ,KAAK,EAAE,gBAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;;;GAQG;AACH,qBACa,yBAAyB;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAMzC,+EAA+E;IAC/E,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;;;OAMG;IACG,QAAQ,CACV,KAAK,EAAE,gBAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CAgC1D;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACtC,KAAK,EAAE,MAAM,EACb,GAAG,OAAO,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAAE,GACtC,QAAQ,CAYV"}
@@ -1,98 +0,0 @@
1
- /**
2
- * The **entry write hook** — the seam a downstream plugin uses to do work
3
- * *inside* an entry's write transaction.
4
- *
5
- * The read-scope port's sibling, and it exists for the same reason the read one
6
- * does: a plugin that derives something from an entry has to derive it where the
7
- * entry is written, or the two can disagree. `@orthacms/segments-server` uses it
8
- * to write an entry's access projection, and the window between "the entry is
9
- * live" and "the rule that hides it is recorded" is exactly the window in which
10
- * restricted content is public.
11
- *
12
- * ## Why not `CONTENT_ENTRY_EXTENSION.afterUpdate`
13
- *
14
- * That hook does precisely this job and would need no new code — except that the
15
- * port is documented as a **single binding**, one provider per app, and i18n
16
- * holds it. Its own note suggests a composite for a second consumer, which is
17
- * fine when the two are peers; here they are not. i18n's implementation returns
18
- * the sibling rows it rewrote, and the writer snapshots a revision for each; a
19
- * composite would have to merge that return value with a second implementation
20
- * that has no opinion about it, and a mistake there writes the wrong revision
21
- * history. This port returns nothing and cannot participate in that decision.
22
- *
23
- * ## The contract
24
- *
25
- * **Runs in the write's own transaction.** The executor handed to the hook is
26
- * the transaction the row was written in, and a throw rolls the write back with
27
- * it. That is the guarantee the hook exists for: an entry and whatever a plugin
28
- * derives from it commit together or not at all.
29
- *
30
- * **Runs after the row and its relations are written**, so the hook sees the
31
- * entry in its final state — including any columns the entries extension
32
- * stamped or rewrote.
33
- *
34
- * **Must be cheap.** It is on the write path of every create and update, and it
35
- * holds the entry's advisory lock while it runs. Work that can wait for a
36
- * commit does not belong here; work that must not be observed separately from
37
- * the write does.
38
- *
39
- * **Deletes are not hooked.** A hard-deleted entry's derived rows are
40
- * unreachable — nothing joins to an id that no longer exists — so the cost of
41
- * leaving them is disk rather than correctness, and paying for a hook on every
42
- * delete to reclaim it is the wrong trade. A plugin that cares reclaims them on
43
- * its own schedule.
44
- */
45
- import { type Provider, type Type } from '@nestjs/common';
46
- import type { Database } from '@orthacms/database';
47
- import type { AnyContentType } from '../types/content-type';
48
- /** The executor the hook must use — the write's own transaction. */
49
- export type EntryWriteExecutor = Parameters<Parameters<Database['transaction']>[0]>[0];
50
- /** What a hook is told about the write it is running inside. */
51
- export interface ContentEntryWriteContext {
52
- /** The write's transaction. Use this, never the shared connection. */
53
- readonly tx: EntryWriteExecutor;
54
- /** The content type written. */
55
- readonly type: AnyContentType;
56
- /** The entry's id. */
57
- readonly entryId: string;
58
- /** The workspace the entry belongs to. */
59
- readonly workspaceId: string;
60
- /** True when the row was just INSERTed; false on an update. */
61
- readonly created: boolean;
62
- }
63
- /** One plugin's participation in an entry write. */
64
- export interface ContentEntryWriteHook {
65
- /**
66
- * Runs inside the write transaction, after the row and its relations.
67
- *
68
- * A throw rolls the entry write back. That is deliberate: a hook whose
69
- * failure is swallowed would let the entry commit without whatever the hook
70
- * was meant to derive from it.
71
- */
72
- afterWrite(context: ContentEntryWriteContext): Promise<void>;
73
- }
74
- /** Every registered write hook, in registration order. */
75
- export declare class ContentEntryWriteHookRegistry {
76
- private readonly hooks;
77
- /** Adds a hook. Registering the same instance twice is a no-op. */
78
- register(hook: ContentEntryWriteHook): void;
79
- /** Every registered hook. Empty on an installation with none. */
80
- all(): readonly ContentEntryWriteHook[];
81
- /**
82
- * Runs every hook, in order, inside the caller's transaction.
83
- *
84
- * Sequential rather than concurrent: the hooks share one transaction, and a
85
- * Postgres connection carries one statement at a time — firing them at once
86
- * would only interleave their awaits onto the same connection.
87
- */
88
- run(context: ContentEntryWriteContext): Promise<void>;
89
- }
90
- /**
91
- * Builds the DI provider that registers `hooks` with content's write-hook
92
- * registry at bootstrap. The sibling of `contentReadScopeRegistrar`, and a
93
- * factory with an explicit `inject` list for the same reason: a reflected
94
- * `Foo | null` parameter type emits `Object`, Nest injects `undefined`, and the
95
- * plugin silently registers nothing.
96
- */
97
- export declare function contentEntryWriteHookRegistrar(label: string, ...hooks: Type<ContentEntryWriteHook>[]): Provider;
98
- //# sourceMappingURL=entry-write-hook.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry-write-hook.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/entry-write-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACvC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CACzC,CAAC,CAAC,CAAC,CAAC;AAEL,gEAAgE;AAChE,MAAM,WAAW,wBAAwB;IACrC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,EAAE,kBAAkB,CAAC;IAChC,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC7B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;;;OAMG;IACH,UAAU,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAED,0DAA0D;AAC1D,qBACa,6BAA6B;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IAErD,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI;IAM3C,iEAAiE;IACjE,GAAG,IAAI,SAAS,qBAAqB,EAAE;IAIvC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;CAK9D;AAyBD;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC1C,KAAK,EAAE,MAAM,EACb,GAAG,KAAK,EAAE,IAAI,CAAC,qBAAqB,CAAC,EAAE,GACxC,QAAQ,CAYV"}
@@ -1,18 +0,0 @@
1
- import { type AnyFieldSpec } from '../types/fields';
2
- import type { AnyContentType } from '../types/content-type';
3
- /**
4
- * Whether a field is a **per-locale relation** — a *single owning* relation
5
- * (`!many`, `!inverse`) on an **i18n** owner whose **target type is also i18n**.
6
- *
7
- * Such a relation can't be shared across a translation group: a shared FK would
8
- * point at one specific locale's row, i.e. a cross-locale link. So the platform
9
- * treats it as `localized` — not synced onto siblings, not copied into a
10
- * translation draft, and the picker offers only same-locale candidates. Both the
11
- * schema serializer (which stamps `localized` for the admin) and the i18n
12
- * sibling-sync consult this, so the rule lives in one place.
13
- *
14
- * The target's i18n-ness is read via the resolved thunk `spec.relation.to()` —
15
- * safe because the registry resolves every relation target at boot.
16
- */
17
- export declare function isPerLocaleRelation(owner: AnyContentType, spec: AnyFieldSpec): boolean;
18
- //# sourceMappingURL=per-locale-relation.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"per-locale-relation.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/per-locale-relation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,YAAY,GACnB,OAAO,CAQT"}