@orthacms/content-server 0.4.2 → 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 (246) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +41 -1
  4. package/dist/lib/content.module.d.ts.map +1 -1
  5. package/dist/lib/content.module.js +31 -1
  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/http/controllers/create-entry.controller.d.ts.map +1 -1
  50. package/dist/lib/entries/http/controllers/create-entry.controller.js +1 -1
  51. package/dist/lib/entries/http/controllers/update-entry.controller.d.ts.map +1 -1
  52. package/dist/lib/entries/http/controllers/update-entry.controller.js +1 -1
  53. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts +14 -0
  54. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts.map +1 -0
  55. package/dist/lib/entries/http/dto/extension-bag.validator.js +42 -0
  56. package/dist/lib/entries/http/dto/save-entry.dto.d.ts +16 -0
  57. package/dist/lib/entries/http/dto/save-entry.dto.d.ts.map +1 -1
  58. package/dist/lib/entries/http/dto/save-entry.dto.js +30 -0
  59. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts +50 -8
  60. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
  61. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +183 -32
  62. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts +38 -5
  63. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts.map +1 -1
  64. package/dist/lib/entries/infrastructure/persistence/relation-link.service.js +38 -6
  65. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts +3 -1
  66. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts.map +1 -1
  67. package/dist/lib/entries/infrastructure/queries/entries.service.js +17 -5
  68. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts +82 -0
  69. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts.map +1 -0
  70. package/dist/lib/entries/infrastructure/queries/entry-match.query.js +152 -0
  71. package/dist/lib/extension/entry-filter-provider.d.ts +79 -0
  72. package/dist/lib/extension/entry-filter-provider.d.ts.map +1 -0
  73. package/dist/lib/extension/entry-filter-provider.js +149 -0
  74. package/dist/lib/extension/entry-write-extension.d.ts +185 -0
  75. package/dist/lib/extension/entry-write-extension.d.ts.map +1 -0
  76. package/dist/lib/extension/entry-write-extension.js +182 -0
  77. package/dist/lib/extension/read-scope.d.ts +107 -0
  78. package/dist/lib/extension/read-scope.d.ts.map +1 -0
  79. package/dist/lib/extension/read-scope.js +131 -0
  80. package/dist/lib/insights/docs/describe-content-insights-api.d.ts +23 -0
  81. package/dist/lib/insights/docs/describe-content-insights-api.d.ts.map +1 -0
  82. package/dist/lib/insights/docs/describe-content-insights-api.js +329 -0
  83. package/dist/lib/mcp/content-tools.provider.d.ts.map +1 -1
  84. package/dist/lib/mcp/content-tools.provider.js +10 -9
  85. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts +14 -13
  86. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts.map +1 -1
  87. package/dist/lib/public-api/http/controllers/public-entry-writes.controller.js +62 -46
  88. package/dist/lib/public-api/http/guards/api-token-workspace.guard.d.ts.map +1 -1
  89. package/dist/lib/public-api/http/guards/api-token-workspace.guard.js +8 -0
  90. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts +14 -1
  91. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts.map +1 -1
  92. package/dist/lib/public-api/infrastructure/public-entries.query.js +20 -3
  93. package/dist/lib/public-api/infrastructure/public-entry-writes.service.d.ts +10 -9
  94. package/dist/lib/public-api/infrastructure/public-entry-writes.service.d.ts.map +1 -1
  95. package/dist/lib/public-api/infrastructure/public-entry-writes.service.js +23 -24
  96. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts +16 -4
  97. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts.map +1 -1
  98. package/dist/lib/public-api/infrastructure/public-expansion.query.js +29 -9
  99. package/dist/lib/public-api/infrastructure/token-actor.d.ts +37 -0
  100. package/dist/lib/public-api/infrastructure/token-actor.d.ts.map +1 -0
  101. package/dist/lib/public-api/infrastructure/token-actor.js +58 -0
  102. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.d.ts.map +1 -1
  103. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.js +23 -1
  104. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts +7 -1
  105. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts.map +1 -1
  106. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.js +9 -2
  107. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.d.ts +28 -0
  108. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.d.ts.map +1 -0
  109. package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.js +52 -0
  110. package/dist/lib/revisions/types/revision-view.d.ts +11 -0
  111. package/dist/lib/revisions/types/revision-view.d.ts.map +1 -1
  112. package/dist/lib/utils/content-plugin.d.ts.map +1 -1
  113. package/dist/lib/utils/content-plugin.js +10 -1
  114. package/dist/lib/utils/content-views-plugin.d.ts +28 -0
  115. package/dist/lib/utils/content-views-plugin.d.ts.map +1 -0
  116. package/dist/lib/utils/content-views-plugin.js +37 -0
  117. package/dist/lib/views/application/queries/saved-views.query.d.ts +16 -0
  118. package/dist/lib/views/application/queries/saved-views.query.d.ts.map +1 -0
  119. package/dist/lib/views/application/queries/saved-views.query.js +46 -0
  120. package/dist/lib/views/application/saved-view-access.service.d.ts +30 -0
  121. package/dist/lib/views/application/saved-view-access.service.d.ts.map +1 -0
  122. package/dist/lib/views/application/saved-view-access.service.js +55 -0
  123. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts +30 -0
  124. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts.map +1 -0
  125. package/dist/lib/views/application/use-cases/create-saved-view.use-case.js +81 -0
  126. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +18 -0
  127. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts.map +1 -0
  128. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.js +56 -0
  129. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts +23 -0
  130. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts.map +1 -0
  131. package/dist/lib/views/application/use-cases/set-default-view.use-case.js +51 -0
  132. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts +24 -0
  133. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts.map +1 -0
  134. package/dist/lib/views/application/use-cases/update-saved-view.use-case.js +90 -0
  135. package/dist/lib/views/application/view-scope.service.d.ts +25 -0
  136. package/dist/lib/views/application/view-scope.service.d.ts.map +1 -0
  137. package/dist/lib/views/application/view-scope.service.js +45 -0
  138. package/dist/lib/views/content-views.module.d.ts +22 -0
  139. package/dist/lib/views/content-views.module.d.ts.map +1 -0
  140. package/dist/lib/views/content-views.module.js +63 -0
  141. package/dist/lib/views/domain/errors/index.d.ts +6 -0
  142. package/dist/lib/views/domain/errors/index.d.ts.map +1 -0
  143. package/dist/lib/views/domain/errors/index.js +12 -0
  144. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts +10 -0
  145. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts.map +1 -0
  146. package/dist/lib/views/domain/errors/saved-view-forbidden.error.js +16 -0
  147. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts +9 -0
  148. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts.map +1 -0
  149. package/dist/lib/views/domain/errors/saved-view-limit-error.js +16 -0
  150. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts +5 -0
  151. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts.map +1 -0
  152. package/dist/lib/views/domain/errors/saved-view-name-taken.error.js +11 -0
  153. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts +9 -0
  154. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts.map +1 -0
  155. package/dist/lib/views/domain/errors/saved-view-not-found.error.js +15 -0
  156. package/dist/lib/views/domain/events/saved-view-events.d.ts +28 -0
  157. package/dist/lib/views/domain/events/saved-view-events.d.ts.map +1 -0
  158. package/dist/lib/views/domain/events/saved-view-events.js +38 -0
  159. package/dist/lib/views/domain/saved-view.d.ts +66 -0
  160. package/dist/lib/views/domain/saved-view.d.ts.map +1 -0
  161. package/dist/lib/views/domain/saved-view.js +18 -0
  162. package/dist/lib/views/domain/saved-view.repository.d.ts +77 -0
  163. package/dist/lib/views/domain/saved-view.repository.d.ts.map +1 -0
  164. package/dist/lib/views/domain/saved-view.repository.js +5 -0
  165. package/dist/lib/views/http/controllers/saved-views.controller.d.ts +45 -0
  166. package/dist/lib/views/http/controllers/saved-views.controller.d.ts.map +1 -0
  167. package/dist/lib/views/http/controllers/saved-views.controller.js +216 -0
  168. package/dist/lib/views/http/controllers/to-http-error.d.ts +10 -0
  169. package/dist/lib/views/http/controllers/to-http-error.d.ts.map +1 -0
  170. package/dist/lib/views/http/controllers/to-http-error.js +26 -0
  171. package/dist/lib/views/http/dto/save-view.dto.d.ts +54 -0
  172. package/dist/lib/views/http/dto/save-view.dto.d.ts.map +1 -0
  173. package/dist/lib/views/http/dto/save-view.dto.js +230 -0
  174. package/dist/lib/views/http/dto/view-payload.validator.d.ts +20 -0
  175. package/dist/lib/views/http/dto/view-payload.validator.d.ts.map +1 -0
  176. package/dist/lib/views/http/dto/view-payload.validator.js +52 -0
  177. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts +26 -0
  178. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts.map +1 -0
  179. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.js +135 -0
  180. package/dist/lib/views/infrastructure/schema/external-refs.d.ts +68 -0
  181. package/dist/lib/views/infrastructure/schema/external-refs.d.ts.map +1 -0
  182. package/dist/lib/views/infrastructure/schema/external-refs.js +29 -0
  183. package/dist/lib/views/infrastructure/schema/index.d.ts +8 -0
  184. package/dist/lib/views/infrastructure/schema/index.d.ts.map +1 -0
  185. package/dist/lib/views/infrastructure/schema/index.js +13 -0
  186. package/dist/lib/views/infrastructure/schema/saved-views.d.ts +275 -0
  187. package/dist/lib/views/infrastructure/schema/saved-views.d.ts.map +1 -0
  188. package/dist/lib/views/infrastructure/schema/saved-views.js +87 -0
  189. package/dist/lib/views/views.constants.d.ts +26 -0
  190. package/dist/lib/views/views.constants.d.ts.map +1 -0
  191. package/dist/lib/views/views.constants.js +28 -0
  192. package/dist/lib/views/views.tokens.d.ts +17 -0
  193. package/dist/lib/views/views.tokens.d.ts.map +1 -0
  194. package/dist/lib/views/views.tokens.js +21 -0
  195. package/migrations/0000_saved_views.sql +29 -0
  196. package/migrations/meta/0000_snapshot.json +256 -0
  197. package/migrations/meta/_journal.json +13 -0
  198. package/package.json +13 -12
  199. package/dist/lib/entries/controllers/bulk-entries.controller.d.ts +0 -26
  200. package/dist/lib/entries/controllers/bulk-entries.controller.d.ts.map +0 -1
  201. package/dist/lib/entries/controllers/create-entry.controller.d.ts +0 -20
  202. package/dist/lib/entries/controllers/create-entry.controller.d.ts.map +0 -1
  203. package/dist/lib/entries/controllers/delete-entry.controller.d.ts +0 -24
  204. package/dist/lib/entries/controllers/delete-entry.controller.d.ts.map +0 -1
  205. package/dist/lib/entries/controllers/get-entry.controller.d.ts +0 -34
  206. package/dist/lib/entries/controllers/get-entry.controller.d.ts.map +0 -1
  207. package/dist/lib/entries/controllers/list-entries.controller.d.ts +0 -20
  208. package/dist/lib/entries/controllers/list-entries.controller.d.ts.map +0 -1
  209. package/dist/lib/entries/controllers/publish-entry.controller.d.ts +0 -19
  210. package/dist/lib/entries/controllers/publish-entry.controller.d.ts.map +0 -1
  211. package/dist/lib/entries/controllers/resolve-type.d.ts +0 -9
  212. package/dist/lib/entries/controllers/resolve-type.d.ts.map +0 -1
  213. package/dist/lib/entries/controllers/update-entry.controller.d.ts +0 -18
  214. package/dist/lib/entries/controllers/update-entry.controller.d.ts.map +0 -1
  215. package/dist/lib/entries/dto/bulk-ids.dto.d.ts +0 -11
  216. package/dist/lib/entries/dto/bulk-ids.dto.d.ts.map +0 -1
  217. package/dist/lib/entries/dto/list-entries-query.dto.d.ts +0 -46
  218. package/dist/lib/entries/dto/list-entries-query.dto.d.ts.map +0 -1
  219. package/dist/lib/entries/dto/relation-delta-map.validator.d.ts +0 -19
  220. package/dist/lib/entries/dto/relation-delta-map.validator.d.ts.map +0 -1
  221. package/dist/lib/entries/dto/save-entry.dto.d.ts +0 -46
  222. package/dist/lib/entries/dto/save-entry.dto.d.ts.map +0 -1
  223. package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts +0 -24
  224. package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts.map +0 -1
  225. package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts +0 -40
  226. package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts.map +0 -1
  227. package/dist/lib/entries/services/entries.service.d.ts +0 -59
  228. package/dist/lib/entries/services/entries.service.d.ts.map +0 -1
  229. package/dist/lib/entries/services/entry-counter.service.d.ts +0 -31
  230. package/dist/lib/entries/services/entry-counter.service.d.ts.map +0 -1
  231. package/dist/lib/entries/services/entry-filter-schema.d.ts +0 -32
  232. package/dist/lib/entries/services/entry-filter-schema.d.ts.map +0 -1
  233. package/dist/lib/entries/services/entry-row.d.ts +0 -56
  234. package/dist/lib/entries/services/entry-row.d.ts.map +0 -1
  235. package/dist/lib/entries/services/entry-writer.service.d.ts +0 -193
  236. package/dist/lib/entries/services/entry-writer.service.d.ts.map +0 -1
  237. package/dist/lib/entries/services/relation-link.service.d.ts +0 -115
  238. package/dist/lib/entries/services/relation-link.service.d.ts.map +0 -1
  239. package/dist/lib/extension/per-locale-relation.d.ts +0 -18
  240. package/dist/lib/extension/per-locale-relation.d.ts.map +0 -1
  241. package/dist/lib/public/controllers/get-public-entry.controller.d.ts +0 -26
  242. package/dist/lib/public/controllers/get-public-entry.controller.d.ts.map +0 -1
  243. package/dist/lib/public/controllers/list-public-entries.controller.d.ts +0 -24
  244. package/dist/lib/public/controllers/list-public-entries.controller.d.ts.map +0 -1
  245. package/dist/lib/public/controllers/public-schema.controller.d.ts +0 -18
  246. package/dist/lib/public/controllers/public-schema.controller.d.ts.map +0 -1
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * The **virtual filter-field registry** — how more than one plugin contributes
4
+ * fields to a content type's `?filter=` surface.
5
+ *
6
+ * `CONTENT_ENTRY_EXTENSION.filterExtension` already declares virtual fields
7
+ * (i18n's `hasLocale` / `missingLocale` / `localeCount`), but that port is a
8
+ * documented **single binding**: one provider per app, and i18n holds it. Nest
9
+ * has no multi-provider, so a second plugin binding the token would silently
10
+ * replace the first — for a filter surface that means fields quietly vanishing
11
+ * from the picker and saved filters starting to 400.
12
+ *
13
+ * So a plugin registers here instead (`@orthacms/segments-server` contributes
14
+ * "can be seen by" / "cannot be seen by" / "restricted"), the same shape and the
15
+ * same reason as `contentReadScopeRegistrar`. The bound extension keeps its
16
+ * method; {@link EntryFilterProviderRegistry.compose} folds the two together
17
+ * into the one `EntryFilterExtension` the query path already knows how to use,
18
+ * so nothing downstream changed.
19
+ *
20
+ * ## What a contribution owes
21
+ *
22
+ * **Every emitted subquery MUST scope to the workspace it is handed.** A virtual
23
+ * field is a subquery over a table this package knows nothing about; one that
24
+ * forgets the workspace turns a filter into a cross-tenant read.
25
+ *
26
+ * **Only declare fields you can answer, with only the operators you support.**
27
+ * The declared `fields` become the SQL whitelist, so an operator the resolver
28
+ * refuses reaches the user as "couldn't load this collection" over a rule the
29
+ * picker itself proposed. Narrow the operator set in the admin's `FilterField`
30
+ * to match.
31
+ *
32
+ * **A filter narrows what is listed; it is not a visibility rule.** Reachability
33
+ * is `CONTENT_READ_SCOPE`'s job and is applied separately. A field here must not
34
+ * be relied on to hide anything.
35
+ */
36
+ var EntryFilterProviderRegistry_1;
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.EntryFilterProviderRegistry = void 0;
39
+ exports.entryFilterProviderRegistrar = entryFilterProviderRegistrar;
40
+ const tslib_1 = require("tslib");
41
+ const common_1 = require("@nestjs/common");
42
+ /** Every registered provider, in registration order. */
43
+ let EntryFilterProviderRegistry = EntryFilterProviderRegistry_1 = class EntryFilterProviderRegistry {
44
+ logger = new common_1.Logger(EntryFilterProviderRegistry_1.name);
45
+ providers = [];
46
+ /** Adds a provider. Registering the same instance twice is a no-op. */
47
+ register(provider) {
48
+ if (!this.providers.includes(provider)) {
49
+ this.providers.push(provider);
50
+ }
51
+ }
52
+ /**
53
+ * Fold the bound extension's contribution and every registered provider's
54
+ * into one {@link EntryFilterExtension}.
55
+ *
56
+ * Returns `undefined` when nobody contributed anything, so a type with no
57
+ * virtual fields builds exactly the surface it did before this registry
58
+ * existed — and `entries.service` keeps its "no extension, no
59
+ * `resolveExtension`" branch.
60
+ *
61
+ * `resolve` routes by the rule's **field name**, to whoever declared it.
62
+ * A name declared twice is kept by its first declarer and the duplicate is
63
+ * dropped with a warning: silently letting the last writer win would make
64
+ * the meaning of a saved filter depend on plugin registration order.
65
+ */
66
+ compose(type, bound) {
67
+ const contributions = [
68
+ ...(bound ? [bound] : []),
69
+ ...this.providers
70
+ .map((provider) => provider.filterFor(type))
71
+ .filter((entry) => !!entry)
72
+ ];
73
+ if (!contributions.length)
74
+ return undefined;
75
+ if (contributions.length === 1)
76
+ return contributions[0];
77
+ const fields = {};
78
+ const owners = new Map();
79
+ for (const contribution of contributions) {
80
+ for (const [name, spec] of Object.entries(contribution.fields)) {
81
+ if (owners.has(name)) {
82
+ this.logger.warn(`Two plugins declare the filter field "${name}" on "${type.name}"; keeping the first.`);
83
+ continue;
84
+ }
85
+ owners.set(name, contribution);
86
+ fields[name] = spec;
87
+ }
88
+ }
89
+ return {
90
+ fields,
91
+ resolve: (rule, context) => {
92
+ const owner = owners.get(rule.path[0]);
93
+ if (!owner) {
94
+ // Unreachable through the parser — a rule only gets here
95
+ // after its name was whitelisted from `fields` above — but a
96
+ // thrown error beats a silently-true predicate if it ever is.
97
+ throw new Error(`No provider owns the filter field "${rule.path[0]}".`);
98
+ }
99
+ return owner.resolve(rule, context);
100
+ }
101
+ };
102
+ }
103
+ };
104
+ exports.EntryFilterProviderRegistry = EntryFilterProviderRegistry;
105
+ exports.EntryFilterProviderRegistry = EntryFilterProviderRegistry = EntryFilterProviderRegistry_1 = tslib_1.__decorate([
106
+ (0, common_1.Injectable)()
107
+ ], EntryFilterProviderRegistry);
108
+ /** Registers a plugin's filter providers with content at bootstrap. */
109
+ class EntryFilterBootstrapper {
110
+ label;
111
+ registry;
112
+ providers;
113
+ logger = new common_1.Logger(EntryFilterBootstrapper.name);
114
+ constructor(label, registry, providers) {
115
+ this.label = label;
116
+ this.registry = registry;
117
+ this.providers = providers;
118
+ }
119
+ onApplicationBootstrap() {
120
+ // A deployment without `ContentPlugin` is not a real configuration, but
121
+ // a binding plugin should still boot rather than fail on an injection it
122
+ // cannot influence.
123
+ if (!this.registry)
124
+ return;
125
+ for (const provider of this.providers) {
126
+ this.registry.register(provider);
127
+ }
128
+ this.logger.log(`Registered the ${this.label} filter fields with content.`);
129
+ }
130
+ }
131
+ /**
132
+ * Builds the DI provider that registers `providers` with content's filter-field
133
+ * registry at bootstrap.
134
+ *
135
+ * A factory with an explicit `inject` list rather than a class with reflected
136
+ * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
137
+ * dependency typed `Foo | null` emits `Object` for `design:paramtypes`, Nest
138
+ * injects `undefined` with no error, and the plugin registers nothing.
139
+ */
140
+ function entryFilterProviderRegistrar(label, ...providers) {
141
+ return {
142
+ provide: `ENTRY_FILTER_PROVIDER_REGISTRAR_${label.toUpperCase()}`,
143
+ useFactory: (registry, ...resolved) => new EntryFilterBootstrapper(label, registry, resolved),
144
+ inject: [
145
+ { token: EntryFilterProviderRegistry, optional: true },
146
+ ...providers
147
+ ]
148
+ };
149
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The **entry-write extension port** — how a downstream plugin stores state of
3
+ * its own *about* an entry, inside the entry's own write transaction and inside
4
+ * the entry's own version history.
5
+ *
6
+ * Content-server declares the port, calls it from `EntryWriterService`, and
7
+ * records what it returns in the revision snapshot's `extra` bag;
8
+ * `@orthacms/segments-server` binds one for reader entitlements, so who may read
9
+ * a record is set on Save, committed with the record, captured by the version,
10
+ * and put back when a version is restored.
11
+ *
12
+ * ## Why not a second HTTP call
13
+ *
14
+ * Nothing forced this port on us — segments already had a `PUT` of its own, and
15
+ * the admin could call it after saving. Three properties are what it buys, none
16
+ * of which a second request has:
17
+ *
18
+ * - **One transaction.** The entry, its links, its audiences and the version
19
+ * recording all three commit together or not at all. Two requests can land
20
+ * half a change: an entry saved and its restriction not applied.
21
+ * - **A truthful version.** A revision is built *inside* the write, so a
22
+ * separate later request could only ever be captured by the **next** save —
23
+ * the version would record the access the entry used to have.
24
+ * - **A restorable one.** Restoring version 3 re-applies its snapshot through
25
+ * the same writer, so the extension's own state travels with it. Without that,
26
+ * "go back to Tuesday" would put Tuesday's words in front of today's readers.
27
+ *
28
+ * ## Why a registry rather than a DI token
29
+ *
30
+ * Nest has **no multi-provider**: two dynamic modules binding one token do not
31
+ * merge, the second silently replaces the first. For this port the loss would be
32
+ * silent in the worst way — an extension that stopped being called writes
33
+ * nothing and captures nothing, so a restriction quietly stops applying. So a
34
+ * plugin registers through {@link entryWriteExtensionRegistrar}, the same shape
35
+ * and the same reason as `contentReadScopeRegistrar` and `copilotToolsRegistrar`.
36
+ *
37
+ * ## Why not `CONTENT_ENTRY_EXTENSION`
38
+ *
39
+ * That port is a documented **single binding** and i18n holds it. It also
40
+ * answers a different question: it extends how *this package's own* row is
41
+ * written (envelope columns, sibling sync). This one is about state a
42
+ * **different plugin** owns, in a table content-server knows nothing about, and
43
+ * the two must be able to coexist — an i18n installation with segments enabled
44
+ * is the ordinary case, not a conflict.
45
+ *
46
+ * ## Two rules an implementation must keep
47
+ *
48
+ * **Use the executor you are handed, never a connection of your own.** It is the
49
+ * save's transaction; a write on any other connection commits separately and
50
+ * takes the atomicity above with it.
51
+ *
52
+ * **`capture` is called for every snapshot, `apply` only when asked.** A save
53
+ * that carried nothing for an extension must leave that extension's state alone
54
+ * — but its *version* still has to record what the state was, or restoring that
55
+ * version would read as "this had no audiences" and open the entry up.
56
+ */
57
+ import { type Provider, type Type } from '@nestjs/common';
58
+ import type { DbTransaction } from '../entries/infrastructure/persistence/relation-link.service';
59
+ import type { AnyContentType } from '../types/content-type';
60
+ /** The entry an extension is reading or writing state for. */
61
+ export interface EntryWriteExtensionTarget {
62
+ /**
63
+ * The save's own transaction. Every read and write an extension makes MUST
64
+ * go through it — a write on another connection commits on its own, which is
65
+ * the whole thing this port exists to prevent.
66
+ */
67
+ readonly executor: DbTransaction;
68
+ /** The content type being saved. */
69
+ readonly type: AnyContentType;
70
+ /** The row's id. On a create, the row that has just been inserted. */
71
+ readonly entryId: string;
72
+ /** The workspace the entry belongs to. */
73
+ readonly workspaceId: string;
74
+ }
75
+ /** A write: the target, plus what the caller sent for this extension. */
76
+ export interface EntryWriteExtensionInput extends EntryWriteExtensionTarget {
77
+ /**
78
+ * The caller's value under this extension's {@link EntryWriteExtension.key},
79
+ * verbatim and **unvalidated** — content-server declares the bag as opaque
80
+ * and never looks inside it, exactly as it does for `locale`. An extension
81
+ * validates its own slice and throws a `BadRequestException` /
82
+ * `UnprocessableEntityException` on a bad one, which rolls the save back.
83
+ *
84
+ * On a **restore** this is the value the restored version captured, so the
85
+ * same code path puts state back that put it there.
86
+ */
87
+ readonly value: unknown;
88
+ }
89
+ /** One plugin's state alongside an entry, written and versioned with it. */
90
+ export interface EntryWriteExtension {
91
+ /**
92
+ * The extension's slot — in the request's `extensions` bag and in the
93
+ * revision snapshot's `extra`. Must be stable: a stored snapshot names it,
94
+ * so renaming the key orphans every version already captured.
95
+ */
96
+ readonly key: string;
97
+ /**
98
+ * Write this extension's state for the entry, on the save's transaction.
99
+ *
100
+ * Called **only** when the request carried a value under {@link key} — a
101
+ * save that said nothing about audiences must not clear them.
102
+ *
103
+ * Return the ids of **other** entries this write also changed — the locale
104
+ * siblings segments writes an entry's audiences to, for instance. Content
105
+ * appends a revision for each, exactly as it does for the rows i18n's shared
106
+ * -field sync rewrote: a row whose stored state moved while its timeline did
107
+ * not is a history that hides the change and, on the next restore, undoes
108
+ * it. Return nothing when only the named entry changed.
109
+ */
110
+ apply(input: EntryWriteExtensionInput): Promise<readonly string[] | void>;
111
+ /**
112
+ * What the revision should record, read back on the same transaction so it
113
+ * reflects what actually committed.
114
+ *
115
+ * Called for **every** snapshot, including a save that never mentioned this
116
+ * extension and including the locale siblings an extension rewrote — a
117
+ * version that omitted the state it did not change would read, on restore,
118
+ * as a version that had none.
119
+ *
120
+ * Return `undefined` to record nothing (the key is then absent from `extra`,
121
+ * which is what an unconfigured installation should produce).
122
+ */
123
+ capture(target: EntryWriteExtensionTarget): Promise<unknown>;
124
+ /**
125
+ * A row has just been **inserted** — a chance to give it whatever the rest
126
+ * of its locale group already has.
127
+ *
128
+ * Optional, and called on every create **after** {@link apply}, so an
129
+ * extension that has just been told what to store is not asked to inherit
130
+ * over it. It exists because `apply` runs only for keys the caller sent, and
131
+ * "create a translation" sends none: without this, translating a restricted
132
+ * article produced a public German copy of it, which is the failure mode a
133
+ * reader notices and an editor never does.
134
+ *
135
+ * The equivalent for content's own columns is `CONTENT_ENTRY_EXTENSION`'s
136
+ * create-only relation inheritance; this is the same posture for state a
137
+ * different plugin owns.
138
+ */
139
+ inherit?(target: EntryWriteExtensionTarget): Promise<void>;
140
+ }
141
+ /** Every registered extension, in registration order. */
142
+ export declare class EntryWriteExtensionRegistry {
143
+ private readonly extensions;
144
+ /** Adds an extension. Registering the same instance twice is a no-op. */
145
+ register(extension: EntryWriteExtension): void;
146
+ /** Whether anything is registered — the check that keeps a bare install free. */
147
+ get configured(): boolean;
148
+ /**
149
+ * Run every extension the caller sent a value for.
150
+ *
151
+ * An unknown key is **ignored**, not an error: the bag is forwarded from a
152
+ * client that may be talking to an installation without that plugin, and
153
+ * failing the whole save over it would make a request that works on one
154
+ * deployment a 400 on another. What the save then records is the truth —
155
+ * `capture` reports no state for a key nothing owns.
156
+ */
157
+ applyAll(target: EntryWriteExtensionTarget, inputs: Record<string, unknown> | undefined): Promise<string[]>;
158
+ /**
159
+ * Let every extension that wants to give a **just-inserted** row what its
160
+ * locale group already holds.
161
+ *
162
+ * Runs after {@link applyAll} on a create, and only there: an extension the
163
+ * caller just told what to store must not then inherit over it.
164
+ */
165
+ inheritAll(target: EntryWriteExtensionTarget): Promise<void>;
166
+ /**
167
+ * Everything the extensions hold for one entry, keyed by extension.
168
+ *
169
+ * Returns `undefined` — not an empty object — when there is nothing to
170
+ * record, so a snapshot taken on an installation with no extensions is
171
+ * byte-for-byte what it was before this port existed.
172
+ */
173
+ captureAll(target: EntryWriteExtensionTarget): Promise<Record<string, unknown> | undefined>;
174
+ }
175
+ /**
176
+ * Builds the DI provider that registers `extensions` with content's entry-write
177
+ * registry at bootstrap.
178
+ *
179
+ * A factory with an explicit `inject` list rather than a class with reflected
180
+ * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
181
+ * dependency typed `Foo | null` emits `Object` for `design:paramtypes`, Nest
182
+ * injects `undefined` with no error, and the plugin registers nothing.
183
+ */
184
+ export declare function entryWriteExtensionRegistrar(label: string, ...extensions: Type<EntryWriteExtension>[]): Provider;
185
+ //# sourceMappingURL=entry-write-extension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-write-extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/entry-write-extension.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6DAA6D,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,WAAW,yBAAyB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC;AAED,yEAAyE;AACzE,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACvE;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CAC3B;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAChC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IAE1E;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7D;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D;AAED,yDAAyD;AACzD,qBACa,2BAA2B;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,mBAAmB,GAAG,IAAI;IAM9C,iFAAiF;IACjF,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;;;;;;OAQG;IACG,QAAQ,CACV,MAAM,EAAE,yBAAyB,EACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5C,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBpB;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlE;;;;;;OAMG;IACG,UAAU,CACZ,MAAM,EAAE,yBAAyB,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CASlD;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CACxC,KAAK,EAAE,MAAM,EACb,GAAG,UAAU,EAAE,IAAI,CAAC,mBAAmB,CAAC,EAAE,GAC3C,QAAQ,CAYV"}
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * The **entry-write extension port** — how a downstream plugin stores state of
4
+ * its own *about* an entry, inside the entry's own write transaction and inside
5
+ * the entry's own version history.
6
+ *
7
+ * Content-server declares the port, calls it from `EntryWriterService`, and
8
+ * records what it returns in the revision snapshot's `extra` bag;
9
+ * `@orthacms/segments-server` binds one for reader entitlements, so who may read
10
+ * a record is set on Save, committed with the record, captured by the version,
11
+ * and put back when a version is restored.
12
+ *
13
+ * ## Why not a second HTTP call
14
+ *
15
+ * Nothing forced this port on us — segments already had a `PUT` of its own, and
16
+ * the admin could call it after saving. Three properties are what it buys, none
17
+ * of which a second request has:
18
+ *
19
+ * - **One transaction.** The entry, its links, its audiences and the version
20
+ * recording all three commit together or not at all. Two requests can land
21
+ * half a change: an entry saved and its restriction not applied.
22
+ * - **A truthful version.** A revision is built *inside* the write, so a
23
+ * separate later request could only ever be captured by the **next** save —
24
+ * the version would record the access the entry used to have.
25
+ * - **A restorable one.** Restoring version 3 re-applies its snapshot through
26
+ * the same writer, so the extension's own state travels with it. Without that,
27
+ * "go back to Tuesday" would put Tuesday's words in front of today's readers.
28
+ *
29
+ * ## Why a registry rather than a DI token
30
+ *
31
+ * Nest has **no multi-provider**: two dynamic modules binding one token do not
32
+ * merge, the second silently replaces the first. For this port the loss would be
33
+ * silent in the worst way — an extension that stopped being called writes
34
+ * nothing and captures nothing, so a restriction quietly stops applying. So a
35
+ * plugin registers through {@link entryWriteExtensionRegistrar}, the same shape
36
+ * and the same reason as `contentReadScopeRegistrar` and `copilotToolsRegistrar`.
37
+ *
38
+ * ## Why not `CONTENT_ENTRY_EXTENSION`
39
+ *
40
+ * That port is a documented **single binding** and i18n holds it. It also
41
+ * answers a different question: it extends how *this package's own* row is
42
+ * written (envelope columns, sibling sync). This one is about state a
43
+ * **different plugin** owns, in a table content-server knows nothing about, and
44
+ * the two must be able to coexist — an i18n installation with segments enabled
45
+ * is the ordinary case, not a conflict.
46
+ *
47
+ * ## Two rules an implementation must keep
48
+ *
49
+ * **Use the executor you are handed, never a connection of your own.** It is the
50
+ * save's transaction; a write on any other connection commits separately and
51
+ * takes the atomicity above with it.
52
+ *
53
+ * **`capture` is called for every snapshot, `apply` only when asked.** A save
54
+ * that carried nothing for an extension must leave that extension's state alone
55
+ * — but its *version* still has to record what the state was, or restoring that
56
+ * version would read as "this had no audiences" and open the entry up.
57
+ */
58
+ Object.defineProperty(exports, "__esModule", { value: true });
59
+ exports.EntryWriteExtensionRegistry = void 0;
60
+ exports.entryWriteExtensionRegistrar = entryWriteExtensionRegistrar;
61
+ const tslib_1 = require("tslib");
62
+ const common_1 = require("@nestjs/common");
63
+ /** Every registered extension, in registration order. */
64
+ let EntryWriteExtensionRegistry = class EntryWriteExtensionRegistry {
65
+ extensions = [];
66
+ /** Adds an extension. Registering the same instance twice is a no-op. */
67
+ register(extension) {
68
+ if (!this.extensions.includes(extension)) {
69
+ this.extensions.push(extension);
70
+ }
71
+ }
72
+ /** Whether anything is registered — the check that keeps a bare install free. */
73
+ get configured() {
74
+ return this.extensions.length > 0;
75
+ }
76
+ /**
77
+ * Run every extension the caller sent a value for.
78
+ *
79
+ * An unknown key is **ignored**, not an error: the bag is forwarded from a
80
+ * client that may be talking to an installation without that plugin, and
81
+ * failing the whole save over it would make a request that works on one
82
+ * deployment a 400 on another. What the save then records is the truth —
83
+ * `capture` reports no state for a key nothing owns.
84
+ */
85
+ async applyAll(target, inputs) {
86
+ if (!inputs || !this.extensions.length)
87
+ return [];
88
+ // Every *other* entry the extensions changed, so the caller can give
89
+ // each one the revision its moved state deserves. Deduplicated, and the
90
+ // named entry is dropped: it gets its own snapshot either way.
91
+ const alsoChanged = new Set();
92
+ for (const extension of this.extensions) {
93
+ if (!(extension.key in inputs))
94
+ continue;
95
+ const touched = await extension.apply({
96
+ ...target,
97
+ value: inputs[extension.key]
98
+ });
99
+ for (const id of touched ?? []) {
100
+ if (id !== target.entryId)
101
+ alsoChanged.add(id);
102
+ }
103
+ }
104
+ return [...alsoChanged];
105
+ }
106
+ /**
107
+ * Let every extension that wants to give a **just-inserted** row what its
108
+ * locale group already holds.
109
+ *
110
+ * Runs after {@link applyAll} on a create, and only there: an extension the
111
+ * caller just told what to store must not then inherit over it.
112
+ */
113
+ async inheritAll(target) {
114
+ for (const extension of this.extensions) {
115
+ await extension.inherit?.(target);
116
+ }
117
+ }
118
+ /**
119
+ * Everything the extensions hold for one entry, keyed by extension.
120
+ *
121
+ * Returns `undefined` — not an empty object — when there is nothing to
122
+ * record, so a snapshot taken on an installation with no extensions is
123
+ * byte-for-byte what it was before this port existed.
124
+ */
125
+ async captureAll(target) {
126
+ if (!this.extensions.length)
127
+ return undefined;
128
+ const extra = {};
129
+ for (const extension of this.extensions) {
130
+ const value = await extension.capture(target);
131
+ if (value !== undefined)
132
+ extra[extension.key] = value;
133
+ }
134
+ return Object.keys(extra).length ? extra : undefined;
135
+ }
136
+ };
137
+ exports.EntryWriteExtensionRegistry = EntryWriteExtensionRegistry;
138
+ exports.EntryWriteExtensionRegistry = EntryWriteExtensionRegistry = tslib_1.__decorate([
139
+ (0, common_1.Injectable)()
140
+ ], EntryWriteExtensionRegistry);
141
+ /** Registers a plugin's entry-write extensions with content at bootstrap. */
142
+ class EntryWriteExtensionBootstrapper {
143
+ label;
144
+ registry;
145
+ extensions;
146
+ logger = new common_1.Logger(EntryWriteExtensionBootstrapper.name);
147
+ constructor(label, registry, extensions) {
148
+ this.label = label;
149
+ this.registry = registry;
150
+ this.extensions = extensions;
151
+ }
152
+ onApplicationBootstrap() {
153
+ // A deployment without `ContentPlugin` is not a real configuration, but
154
+ // a binding plugin should still boot rather than fail on an injection it
155
+ // cannot influence.
156
+ if (!this.registry)
157
+ return;
158
+ for (const extension of this.extensions) {
159
+ this.registry.register(extension);
160
+ }
161
+ this.logger.log(`Registered the ${this.label} entry-write extension with content.`);
162
+ }
163
+ }
164
+ /**
165
+ * Builds the DI provider that registers `extensions` with content's entry-write
166
+ * registry at bootstrap.
167
+ *
168
+ * A factory with an explicit `inject` list rather than a class with reflected
169
+ * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
170
+ * dependency typed `Foo | null` emits `Object` for `design:paramtypes`, Nest
171
+ * injects `undefined` with no error, and the plugin registers nothing.
172
+ */
173
+ function entryWriteExtensionRegistrar(label, ...extensions) {
174
+ return {
175
+ provide: `ENTRY_WRITE_EXTENSION_REGISTRAR_${label.toUpperCase()}`,
176
+ useFactory: (registry, ...resolved) => new EntryWriteExtensionBootstrapper(label, registry, resolved),
177
+ inject: [
178
+ { token: EntryWriteExtensionRegistry, optional: true },
179
+ ...extensions
180
+ ]
181
+ };
182
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The **read scope port** — the seam a downstream plugin uses to narrow what
3
+ * the public content API returns, without this package knowing why.
4
+ *
5
+ * Content-server declares the port and consults it from `PublicEntriesQuery`
6
+ * and `PublicExpansionQuery`; an implementing plugin
7
+ * (`@orthacms/segments-server`, for reader entitlements) registers a scope
8
+ * through {@link contentReadScopeRegistrar}. Every fragment returned is AND-ed
9
+ * onto the visibility predicate the public read already states, so a scope can
10
+ * only ever *subtract* rows — there is no shape of return value that widens a
11
+ * read.
12
+ *
13
+ * ## Why a registry rather than a DI token
14
+ *
15
+ * The port takes any number of independent narrowings, and Nest has **no
16
+ * multi-provider**: two dynamic modules binding the same token do not merge,
17
+ * the second silently replaces the first. A second scoping plugin would
18
+ * therefore switch the first one off, with nothing to notice it — which for a
19
+ * visibility rule means content quietly becoming visible. So registration is a
20
+ * runtime `register(...)` call against a registry this module owns, the same
21
+ * shape and the same reason as `copilotToolsRegistrar`.
22
+ *
23
+ * ## Why not `CONTENT_ENTRY_EXTENSION`
24
+ *
25
+ * That port is documented as a **single binding**: one provider per app, and
26
+ * i18n already holds it. Its own note suggests binding a composite when a
27
+ * second extension appears, and for a write-pipeline extension that composes
28
+ * cleanly — but "narrow a read" and "extend a write" are different
29
+ * responsibilities with different failure modes. A composite would fuse i18n
30
+ * and entitlements into one provider, where a fault in either silently drops
31
+ * the other's clause.
32
+ *
33
+ * ## Two constraints on an implementation
34
+ *
35
+ * **Synchronous.** The predicate is assembled inside the query builder, and
36
+ * making that path async would ripple through every public read for the benefit
37
+ * of one provider. A scope that needs I/O — resolving who the caller is, say —
38
+ * must do it earlier in the request (a guard, an interceptor, middleware) and
39
+ * read the result from its own request-scoped state here.
40
+ *
41
+ * **Public reads only.** These fragments are not applied to the admin's own
42
+ * entries list. An admin caller is a member of the workspace looking at their
43
+ * own CMS, while a reader entitlement is about who may *consume* published
44
+ * content, and scoping the editor's list by it would hide from an author the
45
+ * very rows they are responsible for. The same split content grants already
46
+ * make.
47
+ */
48
+ import { type Provider, type Type } from '@nestjs/common';
49
+ import type { SQL } from 'drizzle-orm';
50
+ import type { AnyContentType } from '../types/content-type';
51
+ /** What a scope is told about the read it is narrowing. */
52
+ export interface ContentReadScopeContext {
53
+ /** The content type being read. Its table is `type.table`. */
54
+ readonly type: AnyContentType;
55
+ /**
56
+ * The workspace the request resolved to. Every emitted fragment MUST stay
57
+ * inside it — a subquery that forgets the workspace turns one tenant's
58
+ * scope into a cross-tenant read.
59
+ */
60
+ readonly workspaceId: string;
61
+ }
62
+ /**
63
+ * One contributed narrowing of the public read.
64
+ *
65
+ * Returning `undefined` means "nothing to add for this read", which is how an
66
+ * implementation opts out per type, per workspace, or entirely — a plugin that
67
+ * is installed but unconfigured must return `undefined` rather than a fragment
68
+ * matching everything, so an unconfigured install pays nothing at all.
69
+ */
70
+ export interface ContentReadScope {
71
+ /** A predicate AND-ed onto the public visibility rule. */
72
+ scope(context: ContentReadScopeContext): SQL | undefined;
73
+ }
74
+ /**
75
+ * Every registered narrowing, in registration order.
76
+ *
77
+ * Order does not affect the result — the fragments are AND-ed — but it is kept
78
+ * stable so a query log reads the same way twice.
79
+ */
80
+ export declare class ContentReadScopeRegistry {
81
+ private readonly scopes;
82
+ /** Adds a scope. Registering the same instance twice is a no-op. */
83
+ register(scope: ContentReadScope): void;
84
+ /** Every registered scope. Empty on an installation with none. */
85
+ all(): readonly ContentReadScope[];
86
+ /**
87
+ * The fragments for one read, ready to be spread into `and(...)`.
88
+ *
89
+ * Returns an empty array — not a fragment matching everything — when
90
+ * nothing is registered, which is the state every installation without a
91
+ * scoping plugin stays in.
92
+ */
93
+ fragments(context: ContentReadScopeContext): (SQL | undefined)[];
94
+ }
95
+ /**
96
+ * Builds the DI provider that registers `scopes` with content's read-scope
97
+ * registry at bootstrap.
98
+ *
99
+ * A factory with an explicit `inject` list rather than a class with reflected
100
+ * parameters, for the reason `copilotToolsRegistrar` documents: an optional
101
+ * dependency typed as `Foo | null` emits `Object` for `design:paramtypes`, Nest
102
+ * injects `undefined` with no error, and the plugin ends up registering
103
+ * nothing. Here that failure would mean a visibility rule that silently stopped
104
+ * being applied.
105
+ */
106
+ export declare function contentReadScopeRegistrar(label: string, ...scopes: Type<ContentReadScope>[]): Provider;
107
+ //# sourceMappingURL=read-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-scope.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/read-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC7B,0DAA0D;IAC1D,KAAK,CAAC,OAAO,EAAE,uBAAuB,GAAG,GAAG,GAAG,SAAS,CAAC;CAC5D;AAED;;;;;GAKG;AACH,qBACa,wBAAwB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IAEjD,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAMvC,kEAAkE;IAClE,GAAG,IAAI,SAAS,gBAAgB,EAAE;IAIlC;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,EAAE,uBAAuB,GAAG,CAAC,GAAG,GAAG,SAAS,CAAC,EAAE;CAMnE;AA4BD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,MAAM,EACb,GAAG,MAAM,EAAE,IAAI,CAAC,gBAAgB,CAAC,EAAE,GACpC,QAAQ,CASV"}