@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.
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -1
- package/dist/lib/content.module.d.ts.map +1 -1
- package/dist/lib/content.module.js +23 -1
- package/dist/lib/entries/http/controllers/create-entry.controller.d.ts.map +1 -1
- package/dist/lib/entries/http/controllers/create-entry.controller.js +1 -1
- package/dist/lib/entries/http/controllers/update-entry.controller.d.ts.map +1 -1
- package/dist/lib/entries/http/controllers/update-entry.controller.js +1 -1
- package/dist/lib/entries/http/dto/extension-bag.validator.d.ts +14 -0
- package/dist/lib/entries/http/dto/extension-bag.validator.d.ts.map +1 -0
- package/dist/lib/entries/http/dto/extension-bag.validator.js +42 -0
- package/dist/lib/entries/http/dto/save-entry.dto.d.ts +16 -0
- package/dist/lib/entries/http/dto/save-entry.dto.d.ts.map +1 -1
- package/dist/lib/entries/http/dto/save-entry.dto.js +30 -0
- package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts +28 -2
- package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
- package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +112 -15
- package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts +38 -5
- package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts.map +1 -1
- package/dist/lib/entries/infrastructure/persistence/relation-link.service.js +38 -6
- package/dist/lib/entries/infrastructure/queries/entries.service.d.ts +3 -1
- package/dist/lib/entries/infrastructure/queries/entries.service.d.ts.map +1 -1
- package/dist/lib/entries/infrastructure/queries/entries.service.js +17 -5
- package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts +82 -0
- package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts.map +1 -0
- package/dist/lib/entries/infrastructure/queries/entry-match.query.js +152 -0
- package/dist/lib/extension/entry-access-source.d.ts +112 -0
- package/dist/lib/extension/entry-access-source.d.ts.map +1 -0
- package/dist/lib/extension/entry-filter-provider.d.ts +79 -0
- package/dist/lib/extension/entry-filter-provider.d.ts.map +1 -0
- package/dist/lib/extension/entry-filter-provider.js +149 -0
- package/dist/lib/extension/entry-write-extension.d.ts +185 -0
- package/dist/lib/extension/entry-write-extension.d.ts.map +1 -0
- package/dist/lib/extension/entry-write-extension.js +182 -0
- package/dist/lib/extension/entry-write-hook.d.ts +98 -0
- package/dist/lib/extension/entry-write-hook.d.ts.map +1 -0
- package/dist/lib/extension/read-scope.d.ts +107 -0
- package/dist/lib/extension/read-scope.d.ts.map +1 -0
- package/dist/lib/extension/read-scope.js +131 -0
- package/dist/lib/public-api/infrastructure/public-entries.query.d.ts +14 -1
- package/dist/lib/public-api/infrastructure/public-entries.query.d.ts.map +1 -1
- package/dist/lib/public-api/infrastructure/public-entries.query.js +20 -3
- package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts +16 -4
- package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts.map +1 -1
- package/dist/lib/public-api/infrastructure/public-expansion.query.js +29 -9
- package/dist/lib/revisions/application/use-cases/restore-revision.use-case.d.ts.map +1 -1
- package/dist/lib/revisions/application/use-cases/restore-revision.use-case.js +12 -1
- package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts +7 -1
- package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts.map +1 -1
- package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.js +9 -2
- package/dist/lib/revisions/types/revision-view.d.ts +11 -0
- package/dist/lib/revisions/types/revision-view.d.ts.map +1 -1
- package/dist/lib/utils/content-views-plugin.d.ts +28 -0
- package/dist/lib/utils/content-views-plugin.d.ts.map +1 -0
- package/dist/lib/utils/content-views-plugin.js +29 -0
- package/dist/lib/views/application/queries/saved-views.query.d.ts +16 -0
- package/dist/lib/views/application/queries/saved-views.query.d.ts.map +1 -0
- package/dist/lib/views/application/queries/saved-views.query.js +46 -0
- package/dist/lib/views/application/saved-view-access.service.d.ts +30 -0
- package/dist/lib/views/application/saved-view-access.service.d.ts.map +1 -0
- package/dist/lib/views/application/saved-view-access.service.js +55 -0
- package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts +27 -0
- package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts.map +1 -0
- package/dist/lib/views/application/use-cases/create-saved-view.use-case.js +57 -0
- package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +15 -0
- package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts.map +1 -0
- package/dist/lib/views/application/use-cases/delete-saved-view.use-case.js +35 -0
- package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts +23 -0
- package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts.map +1 -0
- package/dist/lib/views/application/use-cases/set-default-view.use-case.js +51 -0
- package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts +21 -0
- package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts.map +1 -0
- package/dist/lib/views/application/use-cases/update-saved-view.use-case.js +61 -0
- package/dist/lib/views/application/view-scope.service.d.ts +25 -0
- package/dist/lib/views/application/view-scope.service.d.ts.map +1 -0
- package/dist/lib/views/application/view-scope.service.js +45 -0
- package/dist/lib/views/content-views.module.d.ts +22 -0
- package/dist/lib/views/content-views.module.d.ts.map +1 -0
- package/dist/lib/views/content-views.module.js +63 -0
- package/dist/lib/views/domain/errors/index.d.ts +6 -0
- package/dist/lib/views/domain/errors/index.d.ts.map +1 -0
- package/dist/lib/views/domain/errors/index.js +12 -0
- package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts +10 -0
- package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts.map +1 -0
- package/dist/lib/views/domain/errors/saved-view-forbidden.error.js +16 -0
- package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts +9 -0
- package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts.map +1 -0
- package/dist/lib/views/domain/errors/saved-view-limit-error.js +16 -0
- package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts +5 -0
- package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts.map +1 -0
- package/dist/lib/views/domain/errors/saved-view-name-taken.error.js +11 -0
- package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts +9 -0
- package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts.map +1 -0
- package/dist/lib/views/domain/errors/saved-view-not-found.error.js +15 -0
- package/dist/lib/views/domain/saved-view.d.ts +66 -0
- package/dist/lib/views/domain/saved-view.d.ts.map +1 -0
- package/dist/lib/views/domain/saved-view.js +18 -0
- package/dist/lib/views/domain/saved-view.repository.d.ts +77 -0
- package/dist/lib/views/domain/saved-view.repository.d.ts.map +1 -0
- package/dist/lib/views/domain/saved-view.repository.js +5 -0
- package/dist/lib/views/http/controllers/saved-views.controller.d.ts +45 -0
- package/dist/lib/views/http/controllers/saved-views.controller.d.ts.map +1 -0
- package/dist/lib/views/http/controllers/saved-views.controller.js +216 -0
- package/dist/lib/views/http/controllers/to-http-error.d.ts +10 -0
- package/dist/lib/views/http/controllers/to-http-error.d.ts.map +1 -0
- package/dist/lib/views/http/controllers/to-http-error.js +26 -0
- package/dist/lib/views/http/dto/save-view.dto.d.ts +54 -0
- package/dist/lib/views/http/dto/save-view.dto.d.ts.map +1 -0
- package/dist/lib/views/http/dto/save-view.dto.js +230 -0
- package/dist/lib/views/http/dto/view-payload.validator.d.ts +20 -0
- package/dist/lib/views/http/dto/view-payload.validator.d.ts.map +1 -0
- package/dist/lib/views/http/dto/view-payload.validator.js +52 -0
- package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts +17 -0
- package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts.map +1 -0
- package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.js +125 -0
- package/dist/lib/views/infrastructure/schema/external-refs.d.ts +68 -0
- package/dist/lib/views/infrastructure/schema/external-refs.d.ts.map +1 -0
- package/dist/lib/views/infrastructure/schema/external-refs.js +29 -0
- package/dist/lib/views/infrastructure/schema/index.d.ts +8 -0
- package/dist/lib/views/infrastructure/schema/index.d.ts.map +1 -0
- package/dist/lib/views/infrastructure/schema/index.js +13 -0
- package/dist/lib/views/infrastructure/schema/saved-views.d.ts +275 -0
- package/dist/lib/views/infrastructure/schema/saved-views.d.ts.map +1 -0
- package/dist/lib/views/infrastructure/schema/saved-views.js +87 -0
- package/dist/lib/views/views.constants.d.ts +26 -0
- package/dist/lib/views/views.constants.d.ts.map +1 -0
- package/dist/lib/views/views.constants.js +28 -0
- package/dist/lib/views/views.tokens.d.ts +17 -0
- package/dist/lib/views/views.tokens.d.ts.map +1 -0
- package/dist/lib/views/views.tokens.js +21 -0
- package/migrations/0000_saved_views.sql +29 -0
- package/migrations/meta/0000_snapshot.json +256 -0
- package/migrations/meta/_journal.json +13 -0
- 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,
|
|
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
|
-
|
|
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 = [...
|
|
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
|
|
349
|
-
// shared value landing on them is in
|
|
350
|
-
|
|
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
|
|
514
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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":"
|
|
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
|
-
|
|
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
|
|
648
|
-
*
|
|
649
|
-
*
|
|
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.
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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(
|
|
200
|
-
tslib_1.__param(
|
|
201
|
-
tslib_1.__metadata("design:paramtypes", [Object, relation_link_service_1.RelationLinkService,
|
|
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"}
|