@orthacms/content-server 0.4.1 → 0.4.3

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 (135) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +38 -1
  4. package/dist/lib/content.module.d.ts.map +1 -1
  5. package/dist/lib/content.module.js +23 -1
  6. package/dist/lib/entries/http/controllers/create-entry.controller.d.ts.map +1 -1
  7. package/dist/lib/entries/http/controllers/create-entry.controller.js +1 -1
  8. package/dist/lib/entries/http/controllers/update-entry.controller.d.ts.map +1 -1
  9. package/dist/lib/entries/http/controllers/update-entry.controller.js +1 -1
  10. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts +14 -0
  11. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts.map +1 -0
  12. package/dist/lib/entries/http/dto/extension-bag.validator.js +42 -0
  13. package/dist/lib/entries/http/dto/save-entry.dto.d.ts +16 -0
  14. package/dist/lib/entries/http/dto/save-entry.dto.d.ts.map +1 -1
  15. package/dist/lib/entries/http/dto/save-entry.dto.js +30 -0
  16. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts +28 -2
  17. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
  18. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +112 -15
  19. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts +38 -5
  20. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts.map +1 -1
  21. package/dist/lib/entries/infrastructure/persistence/relation-link.service.js +38 -6
  22. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts +3 -1
  23. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts.map +1 -1
  24. package/dist/lib/entries/infrastructure/queries/entries.service.js +17 -5
  25. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts +82 -0
  26. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts.map +1 -0
  27. package/dist/lib/entries/infrastructure/queries/entry-match.query.js +152 -0
  28. package/dist/lib/extension/entry-access-source.d.ts +112 -0
  29. package/dist/lib/extension/entry-access-source.d.ts.map +1 -0
  30. package/dist/lib/extension/entry-filter-provider.d.ts +79 -0
  31. package/dist/lib/extension/entry-filter-provider.d.ts.map +1 -0
  32. package/dist/lib/extension/entry-filter-provider.js +149 -0
  33. package/dist/lib/extension/entry-write-extension.d.ts +185 -0
  34. package/dist/lib/extension/entry-write-extension.d.ts.map +1 -0
  35. package/dist/lib/extension/entry-write-extension.js +182 -0
  36. package/dist/lib/extension/entry-write-hook.d.ts +98 -0
  37. package/dist/lib/extension/entry-write-hook.d.ts.map +1 -0
  38. package/dist/lib/extension/read-scope.d.ts +107 -0
  39. package/dist/lib/extension/read-scope.d.ts.map +1 -0
  40. package/dist/lib/extension/read-scope.js +131 -0
  41. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts +14 -1
  42. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts.map +1 -1
  43. package/dist/lib/public-api/infrastructure/public-entries.query.js +20 -3
  44. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts +16 -4
  45. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts.map +1 -1
  46. package/dist/lib/public-api/infrastructure/public-expansion.query.js +29 -9
  47. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.d.ts.map +1 -1
  48. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.js +12 -1
  49. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts +7 -1
  50. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts.map +1 -1
  51. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.js +9 -2
  52. package/dist/lib/revisions/types/revision-view.d.ts +11 -0
  53. package/dist/lib/revisions/types/revision-view.d.ts.map +1 -1
  54. package/dist/lib/utils/content-views-plugin.d.ts +28 -0
  55. package/dist/lib/utils/content-views-plugin.d.ts.map +1 -0
  56. package/dist/lib/utils/content-views-plugin.js +29 -0
  57. package/dist/lib/views/application/queries/saved-views.query.d.ts +16 -0
  58. package/dist/lib/views/application/queries/saved-views.query.d.ts.map +1 -0
  59. package/dist/lib/views/application/queries/saved-views.query.js +46 -0
  60. package/dist/lib/views/application/saved-view-access.service.d.ts +30 -0
  61. package/dist/lib/views/application/saved-view-access.service.d.ts.map +1 -0
  62. package/dist/lib/views/application/saved-view-access.service.js +55 -0
  63. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts +27 -0
  64. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts.map +1 -0
  65. package/dist/lib/views/application/use-cases/create-saved-view.use-case.js +57 -0
  66. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +15 -0
  67. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts.map +1 -0
  68. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.js +35 -0
  69. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts +23 -0
  70. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts.map +1 -0
  71. package/dist/lib/views/application/use-cases/set-default-view.use-case.js +51 -0
  72. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts +21 -0
  73. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts.map +1 -0
  74. package/dist/lib/views/application/use-cases/update-saved-view.use-case.js +61 -0
  75. package/dist/lib/views/application/view-scope.service.d.ts +25 -0
  76. package/dist/lib/views/application/view-scope.service.d.ts.map +1 -0
  77. package/dist/lib/views/application/view-scope.service.js +45 -0
  78. package/dist/lib/views/content-views.module.d.ts +22 -0
  79. package/dist/lib/views/content-views.module.d.ts.map +1 -0
  80. package/dist/lib/views/content-views.module.js +63 -0
  81. package/dist/lib/views/domain/errors/index.d.ts +6 -0
  82. package/dist/lib/views/domain/errors/index.d.ts.map +1 -0
  83. package/dist/lib/views/domain/errors/index.js +12 -0
  84. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts +10 -0
  85. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts.map +1 -0
  86. package/dist/lib/views/domain/errors/saved-view-forbidden.error.js +16 -0
  87. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts +9 -0
  88. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts.map +1 -0
  89. package/dist/lib/views/domain/errors/saved-view-limit-error.js +16 -0
  90. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts +5 -0
  91. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts.map +1 -0
  92. package/dist/lib/views/domain/errors/saved-view-name-taken.error.js +11 -0
  93. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts +9 -0
  94. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts.map +1 -0
  95. package/dist/lib/views/domain/errors/saved-view-not-found.error.js +15 -0
  96. package/dist/lib/views/domain/saved-view.d.ts +66 -0
  97. package/dist/lib/views/domain/saved-view.d.ts.map +1 -0
  98. package/dist/lib/views/domain/saved-view.js +18 -0
  99. package/dist/lib/views/domain/saved-view.repository.d.ts +77 -0
  100. package/dist/lib/views/domain/saved-view.repository.d.ts.map +1 -0
  101. package/dist/lib/views/domain/saved-view.repository.js +5 -0
  102. package/dist/lib/views/http/controllers/saved-views.controller.d.ts +45 -0
  103. package/dist/lib/views/http/controllers/saved-views.controller.d.ts.map +1 -0
  104. package/dist/lib/views/http/controllers/saved-views.controller.js +216 -0
  105. package/dist/lib/views/http/controllers/to-http-error.d.ts +10 -0
  106. package/dist/lib/views/http/controllers/to-http-error.d.ts.map +1 -0
  107. package/dist/lib/views/http/controllers/to-http-error.js +26 -0
  108. package/dist/lib/views/http/dto/save-view.dto.d.ts +54 -0
  109. package/dist/lib/views/http/dto/save-view.dto.d.ts.map +1 -0
  110. package/dist/lib/views/http/dto/save-view.dto.js +230 -0
  111. package/dist/lib/views/http/dto/view-payload.validator.d.ts +20 -0
  112. package/dist/lib/views/http/dto/view-payload.validator.d.ts.map +1 -0
  113. package/dist/lib/views/http/dto/view-payload.validator.js +52 -0
  114. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts +17 -0
  115. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts.map +1 -0
  116. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.js +125 -0
  117. package/dist/lib/views/infrastructure/schema/external-refs.d.ts +68 -0
  118. package/dist/lib/views/infrastructure/schema/external-refs.d.ts.map +1 -0
  119. package/dist/lib/views/infrastructure/schema/external-refs.js +29 -0
  120. package/dist/lib/views/infrastructure/schema/index.d.ts +8 -0
  121. package/dist/lib/views/infrastructure/schema/index.d.ts.map +1 -0
  122. package/dist/lib/views/infrastructure/schema/index.js +13 -0
  123. package/dist/lib/views/infrastructure/schema/saved-views.d.ts +275 -0
  124. package/dist/lib/views/infrastructure/schema/saved-views.d.ts.map +1 -0
  125. package/dist/lib/views/infrastructure/schema/saved-views.js +87 -0
  126. package/dist/lib/views/views.constants.d.ts +26 -0
  127. package/dist/lib/views/views.constants.d.ts.map +1 -0
  128. package/dist/lib/views/views.constants.js +28 -0
  129. package/dist/lib/views/views.tokens.d.ts +17 -0
  130. package/dist/lib/views/views.tokens.d.ts.map +1 -0
  131. package/dist/lib/views/views.tokens.js +21 -0
  132. package/migrations/0000_saved_views.sql +29 -0
  133. package/migrations/meta/0000_snapshot.json +256 -0
  134. package/migrations/meta/_journal.json +13 -0
  135. package/package.json +13 -12
@@ -10,6 +10,7 @@ const utils_server_1 = require("@orthacms/utils-server");
10
10
  const content_domain_1 = require("@orthacms/content-domain");
11
11
  const entry_extension_1 = require("../../../extension/entry-extension");
12
12
  const media_asset_resolver_1 = require("../../../extension/media-asset-resolver");
13
+ const entry_write_extension_1 = require("../../../extension/entry-write-extension");
13
14
  const media_accept_1 = require("./media-accept");
14
15
  const content_type_1 = require("../../../types/content-type");
15
16
  const fields_1 = require("../../../types/fields");
@@ -58,19 +59,28 @@ let EntryWriterService = class EntryWriterService {
58
59
  validation;
59
60
  relations;
60
61
  revisionStore;
62
+ writeExtensions;
61
63
  extension;
62
64
  mediaResolver;
63
65
  constructor(db,
64
66
  // The transaction boundary every write now runs inside, and the outbox
65
67
  // that joins it. Together they are what makes an entry's domain event
66
68
  // commit with — and never without — the row it describes.
67
- uow, outbox, validation, relations, revisionStore, extension, mediaResolver) {
69
+ uow, outbox, validation, relations, revisionStore,
70
+ // Every bound entry-write extension (segments' reader entitlements
71
+ // today) — state another plugin owns *about* the entry, written on this
72
+ // save's transaction and captured by its revision. A registry rather
73
+ // than a token because Nest cannot merge two bindings of one token, and
74
+ // an extension that silently stopped being called would silently stop
75
+ // restricting content.
76
+ writeExtensions, extension, mediaResolver) {
68
77
  this.db = db;
69
78
  this.uow = uow;
70
79
  this.outbox = outbox;
71
80
  this.validation = validation;
72
81
  this.relations = relations;
73
82
  this.revisionStore = revisionStore;
83
+ this.writeExtensions = writeExtensions;
74
84
  this.extension = extension;
75
85
  this.mediaResolver = mediaResolver;
76
86
  }
@@ -216,6 +226,17 @@ let EntryWriterService = class EntryWriterService {
216
226
  async appendRevision(tx, type, row, workspaceId, actorId) {
217
227
  const id = row['id'];
218
228
  const links = await this.relations.snapshotLinks(tx, type, row);
229
+ // Whatever the bound extensions hold for this row *now* — read on the
230
+ // save's own transaction, after any `apply` has run, so the version
231
+ // records what committed. Captured unconditionally, including for a
232
+ // sibling row this save only touched indirectly: a version that omitted
233
+ // the state it did not change would restore as a version that had none.
234
+ const extra = await this.writeExtensions.captureAll({
235
+ executor: tx,
236
+ type,
237
+ entryId: id,
238
+ workspaceId
239
+ });
219
240
  await this.revisionStore.lockEntry(tx, id);
220
241
  const number = await this.revisionStore.nextNumber(tx, id);
221
242
  const revision = revision_1.Revision.createDraft({
@@ -225,7 +246,7 @@ let EntryWriterService = class EntryWriterService {
225
246
  localeGroupId: row['localeGroupId'] ?? null,
226
247
  locale: row['locale'] ?? null,
227
248
  number,
228
- snapshot: (0, revision_snapshot_1.buildSnapshot)(type, row, links),
249
+ snapshot: (0, revision_snapshot_1.buildSnapshot)(type, row, links, extra),
229
250
  createdBy: actorId
230
251
  });
231
252
  await this.revisionStore.append(tx, revision);
@@ -241,14 +262,50 @@ let EntryWriterService = class EntryWriterService {
241
262
  * advisory lock, and a stable order across concurrent savers is what keeps
242
263
  * two of them from deadlocking on the pair.
243
264
  */
244
- async appendRevisionsFor(tx, type, rows, workspaceId, actorId) {
245
- if (!rows?.length)
265
+ async appendRevisionsFor(tx, type, rows, workspaceId, actorId,
266
+ /**
267
+ * Ids of further rows an **entry-write extension** changed, which it
268
+ * reports rather than returns whole — it wrote a table of its own, not
269
+ * these ones, so it has no row to hand back. Merged with `rows` and
270
+ * deduplicated by id: one save can reach a sibling through both paths
271
+ * (a shared field *and* an audience), and appending two revisions for it
272
+ * would read as two edits.
273
+ */
274
+ alsoChangedIds = []) {
275
+ const byId = new Map();
276
+ for (const row of rows ?? [])
277
+ byId.set(String(row['id']), row);
278
+ const missing = alsoChangedIds.filter((id) => !byId.has(id));
279
+ for (const row of await this.rowsByIds(tx, type, missing, workspaceId)) {
280
+ byId.set(String(row['id']), row);
281
+ }
282
+ if (!byId.size)
246
283
  return;
247
- const ordered = [...rows].sort((a, b) => String(a['id']).localeCompare(String(b['id'])));
284
+ const ordered = [...byId.values()].sort((a, b) => String(a['id']).localeCompare(String(b['id'])));
248
285
  for (const row of ordered) {
249
286
  await this.appendRevision(tx, type, row, workspaceId, actorId);
250
287
  }
251
288
  }
289
+ /**
290
+ * The rows behind a set of ids, on this transaction.
291
+ *
292
+ * Soft-deleted rows are **included**: a locale in the trash is still a row
293
+ * whose state an extension just changed, and it comes back on restore
294
+ * expecting a timeline that says so.
295
+ */
296
+ async rowsByIds(tx, type, ids, workspaceId) {
297
+ if (!ids.length)
298
+ return [];
299
+ const columns = type.table;
300
+ const id = columns['id'];
301
+ const workspace = columns['workspaceId'];
302
+ if (!id || !workspace)
303
+ return [];
304
+ return (await tx
305
+ .select()
306
+ .from(type.table)
307
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(id, [...ids]), (0, drizzle_orm_1.eq)(workspace, workspaceId))));
308
+ }
252
309
  /**
253
310
  * Create an entry **owned by `workspaceId`** (stamped onto the row so the
254
311
  * reader's workspace filter and every later scoped write see it). A
@@ -266,8 +323,13 @@ let EntryWriterService = class EntryWriterService {
266
323
  * extension validates them and stamps the envelope columns (a
267
324
  * `localeGroupId` makes the row a **sibling** in that translation group). A
268
325
  * duplicate `(group, locale)` surfaces as a **409** via {@link uniqueGuarded}.
326
+ *
327
+ * `extensions` is the same kind of thing one plugin further out: an opaque
328
+ * bag forwarded to the bound {@link EntryWriteExtensionRegistry}, so a plugin
329
+ * that owns state *about* an entry (segments' audiences) writes it inside
330
+ * this transaction and has it captured by the same version.
269
331
  */
270
- async create(type, values, workspaceId, relations, locale, localeGroupId, actor) {
332
+ async create(type, values, workspaceId, relations, locale, localeGroupId, actor, extensions) {
271
333
  const actorId = actor?.id ?? null;
272
334
  // `coerceValues` stamps every declared field onto the bag, so the
273
335
  // "sent in both places" check below has to read the RAW body — after
@@ -342,12 +404,36 @@ let EntryWriterService = class EntryWriterService {
342
404
  // group must land consistent with the group's shared values.
343
405
  // A no-op for a fresh, sibling-less group.
344
406
  const touched = await this.extension?.afterUpdate(tx, type, inserted, coerced, workspaceId, { created: true });
407
+ // Anything a bound plugin was asked to store about this
408
+ // entry — the row exists now, so it finally has something to
409
+ // hang off, and it lands before the snapshot below reads it
410
+ // back. A create is the case that forced this into the write
411
+ // at all: until the insert there is no id to write against.
412
+ const alsoChanged = await this.writeExtensions.applyAll({
413
+ executor: tx,
414
+ type,
415
+ entryId: id,
416
+ workspaceId
417
+ }, extensions);
418
+ // …and, for a row joining an existing locale group, whatever
419
+ // that group already holds. `applyAll` runs only for keys
420
+ // the caller sent, and "create a translation" sends none —
421
+ // so without this a translation of a restricted article was
422
+ // a public copy of it. After `applyAll`, so an extension the
423
+ // caller just told what to store is not overwritten.
424
+ await this.writeExtensions.inheritAll({
425
+ executor: tx,
426
+ type,
427
+ entryId: id,
428
+ workspaceId
429
+ });
345
430
  // Snapshot the just-created document as its first revision,
346
431
  // inside this same transaction.
347
432
  await this.appendRevision(tx, type, inserted, workspaceId, actorId ?? null);
348
- // …and one for every sibling the extension rewrote, so a
349
- // shared value landing on them is in their history too.
350
- await this.appendRevisionsFor(tx, type, touched, workspaceId, actorId ?? null);
433
+ // …and one for every sibling either extension path rewrote,
434
+ // so a shared value — or an audience — landing on them is in
435
+ // their history too.
436
+ await this.appendRevisionsFor(tx, type, touched, workspaceId, actorId ?? null, alsoChanged);
351
437
  // Inside the transaction, so the fact and the row it
352
438
  // describes commit together or not at all — the whole point
353
439
  // of the outbox.
@@ -504,14 +590,25 @@ let EntryWriterService = class EntryWriterService {
504
590
  // locale siblings) run inside the same transaction — a failure
505
591
  // rolls the whole save back.
506
592
  const touched = await this.extension?.afterUpdate(tx, type, updated, coerced, workspaceId, { created: false });
593
+ // Whatever a bound plugin was asked to store about this entry, on
594
+ // this transaction and before the snapshot reads it back — so the
595
+ // version records the audiences the save actually applied rather
596
+ // than the ones it replaced. This runs even when no version is
597
+ // appended (publish-a-version): the state is the entry's, not the
598
+ // revision's.
599
+ const alsoChanged = await this.writeExtensions.applyAll({ executor: tx, type, entryId: id, workspaceId }, options?.extensions);
507
600
  // Snapshot the updated document as a new draft revision, inside this
508
601
  // same transaction — unless the caller is re-applying a version that
509
602
  // already exists in the timeline (publish-a-version).
510
603
  if (options?.appendRevision ?? true) {
511
604
  await this.appendRevision(tx, type, updated, workspaceId, actorId ?? null);
512
605
  }
513
- // …and one for every sibling the extension rewrote.
514
- await this.appendRevisionsFor(tx, type, touched, workspaceId, actorId ?? null);
606
+ // …and one for every sibling either extension path rewrote — the
607
+ // rows i18n synced a shared field to, and the rows a write extension
608
+ // reports it also changed (segments writes an entry's audiences to
609
+ // its whole locale group). Both are the same defect if skipped: a
610
+ // row whose state moved while its timeline did not.
611
+ await this.appendRevisionsFor(tx, type, touched, workspaceId, actorId ?? null, alsoChanged);
515
612
  // A save that changed no field value raises nothing: it is a round
516
613
  // trip, not an editorial change, and the log is read by people.
517
614
  const fields = this.changedFields(type, before, updated);
@@ -1221,12 +1318,12 @@ exports.EntryWriterService = EntryWriterService = tslib_1.__decorate([
1221
1318
  (0, common_1.Injectable)(),
1222
1319
  tslib_1.__param(0, (0, database_1.InjectDatabase)()),
1223
1320
  tslib_1.__param(5, (0, revision_store_1.InjectRevisionStore)()),
1224
- tslib_1.__param(6, (0, common_1.Optional)()),
1225
- tslib_1.__param(6, (0, common_1.Inject)(entry_extension_1.CONTENT_ENTRY_EXTENSION)),
1226
1321
  tslib_1.__param(7, (0, common_1.Optional)()),
1227
- tslib_1.__param(7, (0, media_asset_resolver_1.InjectMediaAssetResolver)()),
1322
+ tslib_1.__param(7, (0, common_1.Inject)(entry_extension_1.CONTENT_ENTRY_EXTENSION)),
1323
+ tslib_1.__param(8, (0, common_1.Optional)()),
1324
+ tslib_1.__param(8, (0, media_asset_resolver_1.InjectMediaAssetResolver)()),
1228
1325
  tslib_1.__metadata("design:paramtypes", [Object, database_1.UnitOfWork,
1229
1326
  database_1.OutboxWriter,
1230
1327
  entry_validation_service_1.EntryValidationService,
1231
- relation_link_service_1.RelationLinkService, Object, Object, Object])
1328
+ relation_link_service_1.RelationLinkService, Object, entry_write_extension_1.EntryWriteExtensionRegistry, Object, Object])
1232
1329
  ], EntryWriterService);
@@ -1,6 +1,7 @@
1
1
  import { type Database } from '@orthacms/database';
2
2
  import { type AnyContentType } from '../../../types/content-type';
3
3
  import { type AnyFieldSpec } from '../../../types/fields';
4
+ import { ContentReadScopeRegistry } from '../../../extension/read-scope';
4
5
  import type { RelationDelta, RelationFieldView, RelationRef } from '../../types/entry-list-view';
5
6
  /**
6
7
  * Reject any target row that lives in a different locale from the source.
@@ -35,7 +36,16 @@ export declare const RELATION_PAGE_SIZE = 20;
35
36
  * links to drafts and needs to see them.
36
37
  */
37
38
  export interface RelationTargetVisibility {
38
- /** Restrict targets to entries that are currently published. */
39
+ /**
40
+ * Restrict targets to entries that are currently published.
41
+ *
42
+ * It also turns on the bound **read scopes** — segments' reader
43
+ * entitlements — against the target type. The two ride one flag because
44
+ * they answer one question ("may this caller reach this target?") and are
45
+ * needed at exactly the same call sites: every public read passes it, and
46
+ * no admin read does. A second flag would be a second thing to remember on
47
+ * a path where forgetting it means counting links a reader cannot follow.
48
+ */
39
49
  publishedOnly?: boolean;
40
50
  }
41
51
  /**
@@ -65,7 +75,8 @@ export type QueryRunner = Pick<Database, 'select'>;
65
75
  */
66
76
  export declare class RelationLinkService {
67
77
  private readonly db;
68
- constructor(db: Database);
78
+ private readonly readScopes?;
79
+ constructor(db: Database, readScopes?: ContentReadScopeRegistry | undefined);
69
80
  /**
70
81
  * First page (+ total) of **every** relation field of `type` for one entry,
71
82
  * keyed by field name — what the editor loads on open. Each field is read
@@ -208,11 +219,33 @@ export declare class RelationLinkService {
208
219
  /**
209
220
  * The visibility predicate applied to a relation's target rows: the
210
221
  * workspace scope, the soft-delete guard, and — only when the caller asks
211
- * for it — the published-only restriction. One definition, so the row read
212
- * (`refsFor`) and the windowed link reads that must *count* correctly can't
213
- * disagree on what a visible target is.
222
+ * for it — the published-only restriction plus the bound read scopes.
223
+ *
224
+ * **One definition**, and that is the load-bearing part. Every path that
225
+ * reads a target goes through it: the row read (`refsFor`), the windowed
226
+ * link reads whose `count(*) over` must not over-report, and the sub-select
227
+ * the join-table reads restrict their window by. So `items` and `total`
228
+ * cannot disagree about what a visible target is — which is exactly what
229
+ * they did while the read scopes were applied only when *hydrating* the
230
+ * items: a reader denied three of five linked records saw two items under a
231
+ * `total` of five, and could infer three restricted records existed.
214
232
  */
215
233
  private targetVisibleWhere;
234
+ /**
235
+ * The bound read scopes' fragments for a relation's **target** type, on a
236
+ * public read.
237
+ *
238
+ * Asked about the target rather than the type the request named: a reader
239
+ * allowed to see an article is not thereby allowed to see everything it
240
+ * points at, and a scope keyed to the source would leave every target
241
+ * unguarded.
242
+ *
243
+ * Gated on `publishedOnly`, which is the flag that marks a public read. The
244
+ * admin's calls pass no visibility and are untouched — an editor must see
245
+ * the records their entry links to in order to manage them, including ones
246
+ * no reader may fetch.
247
+ */
248
+ private readScopeWhere;
216
249
  /**
217
250
  * The id sub-select of a target's visible rows — how a link read that reads
218
251
  * only the JOIN table (so has no target columns of its own) restricts to
@@ -1 +1 @@
1
- {"version":3,"file":"relation-link.service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/entries/infrastructure/persistence/relation-link.service.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAEH,KAAK,cAAc,EAEtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,KAAK,EACR,aAAa,EACb,iBAAiB,EACjB,WAAW,EACd,MAAM,6BAA6B,CAAC;AAMrC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC5B,IAAI,EAAE,GAAG,EAAE,EACX,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,SAAS,GACjC,IAAI,CAcN;AASD,wEAAwE;AACxE,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB;IACrC,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAqBD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,CAClC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CACzC,CAAC,CAAC,CAAC,CAAC;AAEL;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AA0BnD;;;;;;;;;;;;GAYG;AACH,qBACa,mBAAmB;IACE,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAI3D;;;;;;;;;OASG;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;IA2C7C;;;;;;;;;;;;;;;;;OAiBG;IACG,iBAAiB,CACnB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,GAAG,EAAE,EACX,WAAW,EAAE,MAAM,EACnB,QAAQ,SAAqB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACtC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAsD1D;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IA2CpB;;;;;OAKG;YACW,aAAa;IAgD3B;;;;;OAKG;YACW,WAAW;IA6DzB;;;;OAIG;YACW,cAAc;IAsD5B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IA0BpB;;;;;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,EACnB,UAAU,CAAC,EAAE,wBAAwB,GACtC,OAAO,CAAC,iBAAiB,CAAC;IAgG7B;;;;;;;;;;;OAWG;IACG,aAAa,CACf,IAAI,EAAE,QAAQ,GAAG,aAAa,EAC9B,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,GAAG,GACT,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAqBpC;;;;;;;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,EACnB,YAAY,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IA8GhB;;;;;;;;;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,EACnB,YAAY,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IA+ChB;;;;;;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;IAgClB;;;;;;OAMG;YACW,UAAU;IAWxB,sEAAsE;YACxD,YAAY;IAc1B;;;;;;;;OAQG;IACG,WAAW,CACb,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,CAAC;IAIzB;;;;;;OAMG;YACW,OAAO;IA6BrB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAe1B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAUjC,kDAAkD;IAClD,OAAO,CAAC,QAAQ;IAWhB;;;;;;;;;;;;;;OAcG;IACG,mBAAmB,CACrB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAAE,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAAG,SAAS,GACjC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IA+C/B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,qBAAqB,CACvB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IA+D5C;;;;;;;;;;;OAWG;IACG,YAAY,CACd,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,OAAO,CAAC;IAyCnB;;;;;OAKG;IACG,SAAS,CACX,IAAI,EAAE,QAAQ,GAAG,aAAa,EAC9B,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,CAAC;IAapB,wEAAwE;YAC1D,oBAAoB;IA+BlC;;;;;;;;;;;;;;;;;;OAkBG;YACW,aAAa;IAqC3B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IA8BnB;;;;OAIG;IACH,OAAO,CAAC,cAAc;CAezB"}
1
+ {"version":3,"file":"relation-link.service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/entries/infrastructure/persistence/relation-link.service.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ const drizzle_orm_1 = require("drizzle-orm");
8
8
  const database_1 = require("@orthacms/database");
9
9
  const content_type_1 = require("../../../types/content-type");
10
10
  const fields_1 = require("../../../types/fields");
11
+ const read_scope_1 = require("../../../extension/read-scope");
11
12
  const entry_row_1 = require("./entry-row");
12
13
  /**
13
14
  * Reject any target row that lives in a different locale from the source.
@@ -76,8 +77,10 @@ const RELATION_APPEND_LOCK_CLASS = 0x524c; // 'RL'
76
77
  */
77
78
  let RelationLinkService = class RelationLinkService {
78
79
  db;
79
- constructor(db) {
80
+ readScopes;
81
+ constructor(db, readScopes) {
80
82
  this.db = db;
83
+ this.readScopes = readScopes;
81
84
  }
82
85
  // ---- reads -------------------------------------------------------------
83
86
  /**
@@ -644,15 +647,41 @@ let RelationLinkService = class RelationLinkService {
644
647
  /**
645
648
  * The visibility predicate applied to a relation's target rows: the
646
649
  * workspace scope, the soft-delete guard, and — only when the caller asks
647
- * for it — the published-only restriction. One definition, so the row read
648
- * (`refsFor`) and the windowed link reads that must *count* correctly can't
649
- * disagree on what a visible target is.
650
+ * for it — the published-only restriction plus the bound read scopes.
651
+ *
652
+ * **One definition**, and that is the load-bearing part. Every path that
653
+ * reads a target goes through it: the row read (`refsFor`), the windowed
654
+ * link reads whose `count(*) over` must not over-report, and the sub-select
655
+ * the join-table reads restrict their window by. So `items` and `total`
656
+ * cannot disagree about what a visible target is — which is exactly what
657
+ * they did while the read scopes were applied only when *hydrating* the
658
+ * items: a reader denied three of five linked records saw two items under a
659
+ * `total` of five, and could infer three restricted records existed.
650
660
  */
651
661
  targetVisibleWhere(target, workspaceId, visibility) {
652
662
  const cols = target.table;
653
663
  return (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(cols['workspaceId'], workspaceId), cols['deletedAt'] ? (0, drizzle_orm_1.isNull)(cols['deletedAt']) : undefined, visibility?.publishedOnly && target.publishable
654
664
  ? (0, drizzle_orm_1.eq)(cols['status'], content_type_1.ENTRY_STATUS.Published)
655
- : undefined);
665
+ : undefined, ...this.readScopeWhere(target, workspaceId, visibility));
666
+ }
667
+ /**
668
+ * The bound read scopes' fragments for a relation's **target** type, on a
669
+ * public read.
670
+ *
671
+ * Asked about the target rather than the type the request named: a reader
672
+ * allowed to see an article is not thereby allowed to see everything it
673
+ * points at, and a scope keyed to the source would leave every target
674
+ * unguarded.
675
+ *
676
+ * Gated on `publishedOnly`, which is the flag that marks a public read. The
677
+ * admin's calls pass no visibility and are untouched — an editor must see
678
+ * the records their entry links to in order to manage them, including ones
679
+ * no reader may fetch.
680
+ */
681
+ readScopeWhere(target, workspaceId, visibility) {
682
+ if (!visibility?.publishedOnly)
683
+ return [];
684
+ return this.readScopes?.fragments({ type: target, workspaceId }) ?? [];
656
685
  }
657
686
  /**
658
687
  * The id sub-select of a target's visible rows — how a link read that reads
@@ -661,6 +690,8 @@ let RelationLinkService = class RelationLinkService {
661
690
  * when no extra restriction applies, so the predicate collapses away.
662
691
  */
663
692
  visibleTargetIds(target, workspaceId, visibility) {
693
+ // `publishedOnly` is the public-read marker, so it also gates the read
694
+ // scopes (see {@link targetVisibleWhere}) — one check covers both.
664
695
  if (!visibility?.publishedOnly)
665
696
  return undefined;
666
697
  const cols = target.table;
@@ -981,7 +1012,8 @@ exports.RelationLinkService = RelationLinkService;
981
1012
  exports.RelationLinkService = RelationLinkService = tslib_1.__decorate([
982
1013
  (0, common_1.Injectable)(),
983
1014
  tslib_1.__param(0, (0, database_1.InjectDatabase)()),
984
- tslib_1.__metadata("design:paramtypes", [Object])
1015
+ tslib_1.__param(1, (0, common_1.Optional)()),
1016
+ tslib_1.__metadata("design:paramtypes", [Object, read_scope_1.ContentReadScopeRegistry])
985
1017
  ], RelationLinkService);
986
1018
  /**
987
1019
  * The composite map key for "this translation group, in this locale".
@@ -1,5 +1,6 @@
1
1
  import { type Database } from '@orthacms/database';
2
2
  import { type ContentEntryExtension } from '../../../extension/entry-extension';
3
+ import { EntryFilterProviderRegistry } from '../../../extension/entry-filter-provider';
3
4
  import type { AnyContentType } from '../../../types/content-type';
4
5
  import { type ListEntriesQueryDto } from '../../http/dto/list-entries-query.dto';
5
6
  import type { EntryListView } from '../../types/entry-list-view';
@@ -14,8 +15,9 @@ import { RelationLinkService } from '../persistence/relation-link.service';
14
15
  export declare class EntriesService {
15
16
  private readonly db;
16
17
  private readonly relationLinks;
18
+ private readonly filterProviders;
17
19
  private readonly extension?;
18
- constructor(db: Database, relationLinks: RelationLinkService, extension?: ContentEntryExtension | undefined);
20
+ constructor(db: Database, relationLinks: RelationLinkService, filterProviders: EntryFilterProviderRegistry, extension?: ContentEntryExtension | undefined);
19
21
  /**
20
22
  * One page of a collection's entries: search → filter → sort → paginate, run
21
23
  * in SQL against the type's generated table, returned as the
@@ -1 +1 @@
1
- {"version":3,"file":"entries.service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/entries/infrastructure/queries/entries.service.ts"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAEH,KAAK,qBAAqB,EAE7B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,EAGH,KAAK,mBAAmB,EAC3B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAMjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AA+B3E;;;;;;GAMG;AACH,qBACa,cAAc;IAED,OAAO,CAAC,QAAQ,CAAC,EAAE;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAK9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;gBANQ,EAAE,EAAE,QAAQ,EAC9B,aAAa,EAAE,mBAAmB,EAKlC,SAAS,CAAC,EAAE,qBAAqB,YAAA;IAGtD;;;;;;OAMG;IACG,IAAI,CACN,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,mBAAmB,EAC1B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,CAAC;IAgDzB;;;;;;;OAOG;YACW,SAAS;IA0CvB;;;;;;;OAOG;YACW,eAAe;IAyB7B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO;CAsBlB"}
1
+ {"version":3,"file":"entries.service.d.ts","sourceRoot":"","sources":["../../../../../src/lib/entries/infrastructure/queries/entries.service.ts"],"names":[],"mappings":"AAYA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAEH,KAAK,qBAAqB,EAE7B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,EAGH,KAAK,mBAAmB,EAC3B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAMjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AA+B3E;;;;;;GAMG;AACH,qBACa,cAAc;IAED,OAAO,CAAC,QAAQ,CAAC,EAAE;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAI9B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAKhC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAVQ,EAAE,EAAE,QAAQ,EAC9B,aAAa,EAAE,mBAAmB,EAIlC,eAAe,EAAE,2BAA2B,EAK5C,SAAS,CAAC,EAAE,qBAAqB,YAAA;IAGtD;;;;;;OAMG;IACG,IAAI,CACN,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,mBAAmB,EAC1B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,CAAC;IAgDzB;;;;;;;OAOG;YACW,SAAS;IAiDvB;;;;;;;OAOG;YACW,eAAe;IAyB7B;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO;CAsBlB"}
@@ -7,6 +7,7 @@ const drizzle_orm_1 = require("drizzle-orm");
7
7
  const database_1 = require("@orthacms/database");
8
8
  const utils_server_1 = require("@orthacms/utils-server");
9
9
  const entry_extension_1 = require("../../../extension/entry-extension");
10
+ const entry_filter_provider_1 = require("../../../extension/entry-filter-provider");
10
11
  const fields_1 = require("../../../types/fields");
11
12
  const list_entries_query_dto_1 = require("../../http/dto/list-entries-query.dto");
12
13
  const entries_constants_1 = require("../../entries.constants");
@@ -43,10 +44,16 @@ function previewFields(type, raw) {
43
44
  let EntriesService = class EntriesService {
44
45
  db;
45
46
  relationLinks;
47
+ filterProviders;
46
48
  extension;
47
- constructor(db, relationLinks, extension) {
49
+ constructor(db, relationLinks,
50
+ // Virtual filter fields contributed by plugins other than the single
51
+ // bound extension — segments' audience fields today. A registry rather
52
+ // than a token, for the reason its own file documents.
53
+ filterProviders, extension) {
48
54
  this.db = db;
49
55
  this.relationLinks = relationLinks;
56
+ this.filterProviders = filterProviders;
50
57
  this.extension = extension;
51
58
  }
52
59
  /**
@@ -105,7 +112,11 @@ let EntriesService = class EntriesService {
105
112
  // into the schema, resolved to its own SQL) and an extra scope
106
113
  // predicate (e.g. the active locale). Both are no-ops for types the
107
114
  // extension doesn't apply to.
108
- const filterExtension = this.extension?.filterExtension(type);
115
+ // The bound extension (i18n) plus every registered provider (segments'
116
+ // audience fields), folded into one contribution — see
117
+ // `EntryFilterProviderRegistry`, which exists because that port is a
118
+ // single binding and a second plugin would otherwise replace it.
119
+ const filterExtension = this.filterProviders.compose(type, this.extension?.filterExtension(type));
109
120
  // Building the surface walks the type's whole relation graph to the
110
121
  // hop budget, so only pay for it when there is actually a filter to
111
122
  // translate — an unfiltered list (the common case) skips it entirely.
@@ -196,7 +207,8 @@ exports.EntriesService = EntriesService;
196
207
  exports.EntriesService = EntriesService = tslib_1.__decorate([
197
208
  (0, common_1.Injectable)(),
198
209
  tslib_1.__param(0, (0, database_1.InjectDatabase)()),
199
- tslib_1.__param(2, (0, common_1.Optional)()),
200
- tslib_1.__param(2, (0, common_1.Inject)(entry_extension_1.CONTENT_ENTRY_EXTENSION)),
201
- tslib_1.__metadata("design:paramtypes", [Object, relation_link_service_1.RelationLinkService, Object])
210
+ tslib_1.__param(3, (0, common_1.Optional)()),
211
+ tslib_1.__param(3, (0, common_1.Inject)(entry_extension_1.CONTENT_ENTRY_EXTENSION)),
212
+ tslib_1.__metadata("design:paramtypes", [Object, relation_link_service_1.RelationLinkService,
213
+ entry_filter_provider_1.EntryFilterProviderRegistry, Object])
202
214
  ], EntriesService);
@@ -0,0 +1,82 @@
1
+ import { type Database } from '@orthacms/database';
2
+ import { type ContentEntryExtension } from '../../../extension/entry-extension';
3
+ import { EntryFilterProviderRegistry } from '../../../extension/entry-filter-provider';
4
+ import type { AnyContentType } from '../../../types/content-type';
5
+ /** Options narrowing a {@link EntryMatchQuery.matchingIds} scan. */
6
+ export interface EntryMatchOptions {
7
+ /**
8
+ * Restrict the scan to these entry ids. This is what turns a
9
+ * collection-wide predicate into the point check an event-driven caller
10
+ * needs — "does *this* row match?" is the same SQL with an extra
11
+ * `id IN (...)`, so one code path serves both and they cannot disagree
12
+ * about what a filter means.
13
+ */
14
+ entryIds?: readonly string[];
15
+ /** Upper bound on returned ids. */
16
+ limit?: number;
17
+ /** Rows to skip — for batching a full-collection scan. */
18
+ offset?: number;
19
+ }
20
+ /**
21
+ * Answers "which entries of this type match this filter?" against the same
22
+ * surface, parser and translator the records list uses.
23
+ *
24
+ * It exists so a downstream plugin can reuse content's filter semantics
25
+ * without reaching for the database or rebuilding the surface: the filterable
26
+ * paths, the relation-hop budget, the workspace scoping of every relation
27
+ * subquery, and the 400 on a malformed tree are all inherited rather than
28
+ * re-implemented. `@orthacms/alarms-server` evaluates its rules through this,
29
+ * which is what makes "the rule I saved from the list means what the list
30
+ * showed me" true by construction rather than by review.
31
+ *
32
+ * Two deliberate differences from the list query:
33
+ *
34
+ * - **No locale scoping.** The entries extension's `listScope` narrows a list
35
+ * to the active locale; a rule is a statement about the collection, so it
36
+ * must see every translation. The extension's *virtual filter fields* are
37
+ * still wired, so a rule may filter on them explicitly.
38
+ * - **Trashed rows are excluded, always.** There is no trash view here, and a
39
+ * tombstoned row is not something anyone can act on.
40
+ */
41
+ export declare class EntryMatchQuery {
42
+ private readonly db;
43
+ private readonly filterProviders;
44
+ private readonly extension?;
45
+ constructor(db: Database, filterProviders: EntryFilterProviderRegistry, extension?: ContentEntryExtension | undefined);
46
+ /**
47
+ * Parses `filter` against `type`'s surface without running it, throwing a
48
+ * `FilterException` when it does not hold up. Callers store filters that
49
+ * will be replayed by a background subscriber for months, so the parse
50
+ * belongs at the moment the filter is written — a tree that only fails at
51
+ * evaluation time fails somewhere nobody is looking.
52
+ */
53
+ assertParses(type: AnyContentType, filter: unknown, workspaceId: string): void;
54
+ /**
55
+ * Ids of `type` in `workspaceId` matching `filter`, oldest-id first so a
56
+ * batched scan is stable across calls.
57
+ */
58
+ matchingIds(type: AnyContentType, filter: unknown, workspaceId: string, options?: EntryMatchOptions): Promise<string[]>;
59
+ /** How many entries of `type` match `filter` right now. */
60
+ countMatching(type: AnyContentType, filter: unknown, workspaceId: string): Promise<number>;
61
+ /**
62
+ * How many entries of `type` the workspace holds at all — the denominator
63
+ * the rule editor shows beside a match count ("14 of 312"), which is what
64
+ * turns a number into a judgement about whether the rule is too broad.
65
+ */
66
+ countAll(type: AnyContentType, workspaceId: string): Promise<number>;
67
+ /**
68
+ * The workspace an entry belongs to, or `null` when no such row exists.
69
+ *
70
+ * A domain event names an entry and its content type but not its workspace
71
+ * — every content table carries its own `workspace_id`, so there is nothing
72
+ * for the envelope to add that a one-column lookup cannot answer. Exposed
73
+ * here rather than left to each subscriber, so a downstream plugin never
74
+ * has a reason to query a `content_<name>` table directly.
75
+ */
76
+ workspaceOf(type: AnyContentType, entryId: string): Promise<string | null>;
77
+ /** The workspace + not-trashed guard every scan starts from. */
78
+ private scope;
79
+ /** Scope + translated filter + optional id restriction. */
80
+ private predicate;
81
+ }
82
+ //# sourceMappingURL=entry-match.query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-match.query.d.ts","sourceRoot":"","sources":["../../../../../src/lib/entries/infrastructure/queries/entry-match.query.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAEH,KAAK,qBAAqB,EAC7B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAalE,oEAAoE;AACpE,MAAM,WAAW,iBAAiB;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,eAAe;IAEF,OAAO,CAAC,QAAQ,CAAC,EAAE;IAIrC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAGhC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAPQ,EAAE,EAAE,QAAQ,EAI9B,eAAe,EAAE,2BAA2B,EAG5C,SAAS,CAAC,EAAE,qBAAqB,YAAA;IAGtD;;;;;;OAMG;IACH,YAAY,CACR,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,MAAM,GACpB,IAAI;IAWP;;;OAGG;IACG,WAAW,CACb,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBpB,2DAA2D;IACrD,aAAa,CACf,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC;IASlB;;;;OAIG;IACG,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ1E;;;;;;;;OAQG;IACG,WAAW,CACb,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUzB,gEAAgE;IAChE,OAAO,CAAC,KAAK;IAQb,2DAA2D;YAC7C,SAAS;CAoC1B"}