@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.
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -1
- package/dist/lib/content.module.d.ts.map +1 -1
- package/dist/lib/content.module.js +31 -1
- package/dist/lib/copilot/entry-proposal.applier.d.ts.map +1 -1
- package/dist/lib/copilot/entry-proposal.applier.js +5 -4
- package/dist/lib/docs/describe-content-api.d.ts +7 -0
- package/dist/lib/docs/describe-content-api.d.ts.map +1 -1
- package/dist/lib/docs/describe-content-api.js +177 -53
- package/dist/lib/docs/describe-views-api.d.ts +19 -0
- package/dist/lib/docs/describe-views-api.d.ts.map +1 -0
- package/dist/lib/docs/describe-views-api.js +76 -0
- package/dist/lib/docs/field-schema.d.ts.map +1 -1
- package/dist/lib/docs/field-schema.js +36 -2
- package/dist/lib/docs/openapi-writer.d.ts +49 -0
- package/dist/lib/docs/openapi-writer.d.ts.map +1 -0
- package/dist/lib/docs/openapi-writer.js +76 -0
- package/dist/lib/docs/public-api-schemas.d.ts +45 -0
- package/dist/lib/docs/public-api-schemas.d.ts.map +1 -0
- package/dist/lib/docs/public-api-schemas.js +377 -0
- package/dist/lib/docs/views-schemas.d.ts +22 -0
- package/dist/lib/docs/views-schemas.d.ts.map +1 -0
- package/dist/lib/docs/views-schemas.js +156 -0
- package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.d.ts +8 -2
- package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.d.ts.map +1 -1
- package/dist/lib/entries/application/use-cases/bulk-publish-entries.use-case.js +19 -4
- package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.d.ts +2 -2
- package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.d.ts.map +1 -1
- package/dist/lib/entries/application/use-cases/bulk-unpublish-entries.use-case.js +14 -4
- package/dist/lib/entries/application/use-cases/publish-entry.use-case.d.ts +6 -8
- package/dist/lib/entries/application/use-cases/publish-entry.use-case.d.ts.map +1 -1
- package/dist/lib/entries/application/use-cases/publish-entry.use-case.js +10 -4
- package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.d.ts +2 -5
- package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.d.ts.map +1 -1
- package/dist/lib/entries/application/use-cases/unpublish-entry.use-case.js +6 -1
- package/dist/lib/entries/domain/entry.d.ts +8 -1
- package/dist/lib/entries/domain/entry.d.ts.map +1 -1
- package/dist/lib/entries/domain/entry.js +12 -12
- package/dist/lib/entries/domain/events/entry-events.d.ts +47 -5
- package/dist/lib/entries/domain/events/entry-events.d.ts.map +1 -1
- package/dist/lib/entries/domain/events/entry-events.js +26 -10
- package/dist/lib/entries/entries.constants.d.ts +10 -2
- package/dist/lib/entries/entries.constants.d.ts.map +1 -1
- package/dist/lib/entries/entries.constants.js +11 -2
- package/dist/lib/entries/http/controllers/bulk-entries.controller.d.ts +2 -2
- package/dist/lib/entries/http/controllers/bulk-entries.controller.d.ts.map +1 -1
- package/dist/lib/entries/http/controllers/bulk-entries.controller.js +8 -6
- 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 +50 -8
- package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
- package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +183 -32
- 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-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/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/insights/docs/describe-content-insights-api.d.ts +23 -0
- package/dist/lib/insights/docs/describe-content-insights-api.d.ts.map +1 -0
- package/dist/lib/insights/docs/describe-content-insights-api.js +329 -0
- package/dist/lib/mcp/content-tools.provider.d.ts.map +1 -1
- package/dist/lib/mcp/content-tools.provider.js +10 -9
- package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts +14 -13
- package/dist/lib/public-api/http/controllers/public-entry-writes.controller.d.ts.map +1 -1
- package/dist/lib/public-api/http/controllers/public-entry-writes.controller.js +62 -46
- package/dist/lib/public-api/http/guards/api-token-workspace.guard.d.ts.map +1 -1
- package/dist/lib/public-api/http/guards/api-token-workspace.guard.js +8 -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-entry-writes.service.d.ts +10 -9
- package/dist/lib/public-api/infrastructure/public-entry-writes.service.d.ts.map +1 -1
- package/dist/lib/public-api/infrastructure/public-entry-writes.service.js +23 -24
- 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/public-api/infrastructure/token-actor.d.ts +37 -0
- package/dist/lib/public-api/infrastructure/token-actor.d.ts.map +1 -0
- package/dist/lib/public-api/infrastructure/token-actor.js +58 -0
- 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 +23 -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/infrastructure/purge/revisions-workspace.purger.d.ts +28 -0
- package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.d.ts.map +1 -0
- package/dist/lib/revisions/infrastructure/purge/revisions-workspace.purger.js +52 -0
- 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-plugin.d.ts.map +1 -1
- package/dist/lib/utils/content-plugin.js +10 -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 +37 -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 +30 -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 +81 -0
- package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +18 -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 +56 -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 +24 -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 +90 -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/events/saved-view-events.d.ts +28 -0
- package/dist/lib/views/domain/events/saved-view-events.d.ts.map +1 -0
- package/dist/lib/views/domain/events/saved-view-events.js +38 -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 +26 -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 +135 -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
- package/dist/lib/entries/controllers/bulk-entries.controller.d.ts +0 -26
- package/dist/lib/entries/controllers/bulk-entries.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/create-entry.controller.d.ts +0 -20
- package/dist/lib/entries/controllers/create-entry.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/delete-entry.controller.d.ts +0 -24
- package/dist/lib/entries/controllers/delete-entry.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/get-entry.controller.d.ts +0 -34
- package/dist/lib/entries/controllers/get-entry.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/list-entries.controller.d.ts +0 -20
- package/dist/lib/entries/controllers/list-entries.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/publish-entry.controller.d.ts +0 -19
- package/dist/lib/entries/controllers/publish-entry.controller.d.ts.map +0 -1
- package/dist/lib/entries/controllers/resolve-type.d.ts +0 -9
- package/dist/lib/entries/controllers/resolve-type.d.ts.map +0 -1
- package/dist/lib/entries/controllers/update-entry.controller.d.ts +0 -18
- package/dist/lib/entries/controllers/update-entry.controller.d.ts.map +0 -1
- package/dist/lib/entries/dto/bulk-ids.dto.d.ts +0 -11
- package/dist/lib/entries/dto/bulk-ids.dto.d.ts.map +0 -1
- package/dist/lib/entries/dto/list-entries-query.dto.d.ts +0 -46
- package/dist/lib/entries/dto/list-entries-query.dto.d.ts.map +0 -1
- package/dist/lib/entries/dto/relation-delta-map.validator.d.ts +0 -19
- package/dist/lib/entries/dto/relation-delta-map.validator.d.ts.map +0 -1
- package/dist/lib/entries/dto/save-entry.dto.d.ts +0 -46
- package/dist/lib/entries/dto/save-entry.dto.d.ts.map +0 -1
- package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts +0 -24
- package/dist/lib/entries/infrastructure/persistence/field-selection.d.ts.map +0 -1
- package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts +0 -40
- package/dist/lib/entries/infrastructure/queries/entry-filter-schema.d.ts.map +0 -1
- package/dist/lib/entries/services/entries.service.d.ts +0 -59
- package/dist/lib/entries/services/entries.service.d.ts.map +0 -1
- package/dist/lib/entries/services/entry-counter.service.d.ts +0 -31
- package/dist/lib/entries/services/entry-counter.service.d.ts.map +0 -1
- package/dist/lib/entries/services/entry-filter-schema.d.ts +0 -32
- package/dist/lib/entries/services/entry-filter-schema.d.ts.map +0 -1
- package/dist/lib/entries/services/entry-row.d.ts +0 -56
- package/dist/lib/entries/services/entry-row.d.ts.map +0 -1
- package/dist/lib/entries/services/entry-writer.service.d.ts +0 -193
- package/dist/lib/entries/services/entry-writer.service.d.ts.map +0 -1
- package/dist/lib/entries/services/relation-link.service.d.ts +0 -115
- package/dist/lib/entries/services/relation-link.service.d.ts.map +0 -1
- package/dist/lib/extension/per-locale-relation.d.ts +0 -18
- package/dist/lib/extension/per-locale-relation.d.ts.map +0 -1
- package/dist/lib/public/controllers/get-public-entry.controller.d.ts +0 -26
- package/dist/lib/public/controllers/get-public-entry.controller.d.ts.map +0 -1
- package/dist/lib/public/controllers/list-public-entries.controller.d.ts +0 -24
- package/dist/lib/public/controllers/list-public-entries.controller.d.ts.map +0 -1
- package/dist/lib/public/controllers/public-schema.controller.d.ts +0 -18
- 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"}
|