@orthacms/content-server 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +38 -1
  4. package/dist/lib/content.module.d.ts.map +1 -1
  5. package/dist/lib/content.module.js +23 -1
  6. package/dist/lib/entries/http/controllers/create-entry.controller.d.ts.map +1 -1
  7. package/dist/lib/entries/http/controllers/create-entry.controller.js +1 -1
  8. package/dist/lib/entries/http/controllers/update-entry.controller.d.ts.map +1 -1
  9. package/dist/lib/entries/http/controllers/update-entry.controller.js +1 -1
  10. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts +14 -0
  11. package/dist/lib/entries/http/dto/extension-bag.validator.d.ts.map +1 -0
  12. package/dist/lib/entries/http/dto/extension-bag.validator.js +42 -0
  13. package/dist/lib/entries/http/dto/save-entry.dto.d.ts +16 -0
  14. package/dist/lib/entries/http/dto/save-entry.dto.d.ts.map +1 -1
  15. package/dist/lib/entries/http/dto/save-entry.dto.js +30 -0
  16. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts +28 -2
  17. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.d.ts.map +1 -1
  18. package/dist/lib/entries/infrastructure/persistence/entry-writer.service.js +112 -15
  19. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts +38 -5
  20. package/dist/lib/entries/infrastructure/persistence/relation-link.service.d.ts.map +1 -1
  21. package/dist/lib/entries/infrastructure/persistence/relation-link.service.js +38 -6
  22. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts +3 -1
  23. package/dist/lib/entries/infrastructure/queries/entries.service.d.ts.map +1 -1
  24. package/dist/lib/entries/infrastructure/queries/entries.service.js +17 -5
  25. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts +82 -0
  26. package/dist/lib/entries/infrastructure/queries/entry-match.query.d.ts.map +1 -0
  27. package/dist/lib/entries/infrastructure/queries/entry-match.query.js +152 -0
  28. package/dist/lib/extension/entry-access-source.d.ts +112 -0
  29. package/dist/lib/extension/entry-access-source.d.ts.map +1 -0
  30. package/dist/lib/extension/entry-filter-provider.d.ts +79 -0
  31. package/dist/lib/extension/entry-filter-provider.d.ts.map +1 -0
  32. package/dist/lib/extension/entry-filter-provider.js +149 -0
  33. package/dist/lib/extension/entry-write-extension.d.ts +185 -0
  34. package/dist/lib/extension/entry-write-extension.d.ts.map +1 -0
  35. package/dist/lib/extension/entry-write-extension.js +182 -0
  36. package/dist/lib/extension/entry-write-hook.d.ts +98 -0
  37. package/dist/lib/extension/entry-write-hook.d.ts.map +1 -0
  38. package/dist/lib/extension/read-scope.d.ts +107 -0
  39. package/dist/lib/extension/read-scope.d.ts.map +1 -0
  40. package/dist/lib/extension/read-scope.js +131 -0
  41. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts +14 -1
  42. package/dist/lib/public-api/infrastructure/public-entries.query.d.ts.map +1 -1
  43. package/dist/lib/public-api/infrastructure/public-entries.query.js +20 -3
  44. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts +16 -4
  45. package/dist/lib/public-api/infrastructure/public-expansion.query.d.ts.map +1 -1
  46. package/dist/lib/public-api/infrastructure/public-expansion.query.js +29 -9
  47. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.d.ts.map +1 -1
  48. package/dist/lib/revisions/application/use-cases/restore-revision.use-case.js +12 -1
  49. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts +7 -1
  50. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.d.ts.map +1 -1
  51. package/dist/lib/revisions/infrastructure/persistence/revision-snapshot.js +9 -2
  52. package/dist/lib/revisions/types/revision-view.d.ts +11 -0
  53. package/dist/lib/revisions/types/revision-view.d.ts.map +1 -1
  54. package/dist/lib/utils/content-views-plugin.d.ts +28 -0
  55. package/dist/lib/utils/content-views-plugin.d.ts.map +1 -0
  56. package/dist/lib/utils/content-views-plugin.js +29 -0
  57. package/dist/lib/views/application/queries/saved-views.query.d.ts +16 -0
  58. package/dist/lib/views/application/queries/saved-views.query.d.ts.map +1 -0
  59. package/dist/lib/views/application/queries/saved-views.query.js +46 -0
  60. package/dist/lib/views/application/saved-view-access.service.d.ts +30 -0
  61. package/dist/lib/views/application/saved-view-access.service.d.ts.map +1 -0
  62. package/dist/lib/views/application/saved-view-access.service.js +55 -0
  63. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts +27 -0
  64. package/dist/lib/views/application/use-cases/create-saved-view.use-case.d.ts.map +1 -0
  65. package/dist/lib/views/application/use-cases/create-saved-view.use-case.js +57 -0
  66. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts +15 -0
  67. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.d.ts.map +1 -0
  68. package/dist/lib/views/application/use-cases/delete-saved-view.use-case.js +35 -0
  69. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts +23 -0
  70. package/dist/lib/views/application/use-cases/set-default-view.use-case.d.ts.map +1 -0
  71. package/dist/lib/views/application/use-cases/set-default-view.use-case.js +51 -0
  72. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts +21 -0
  73. package/dist/lib/views/application/use-cases/update-saved-view.use-case.d.ts.map +1 -0
  74. package/dist/lib/views/application/use-cases/update-saved-view.use-case.js +61 -0
  75. package/dist/lib/views/application/view-scope.service.d.ts +25 -0
  76. package/dist/lib/views/application/view-scope.service.d.ts.map +1 -0
  77. package/dist/lib/views/application/view-scope.service.js +45 -0
  78. package/dist/lib/views/content-views.module.d.ts +22 -0
  79. package/dist/lib/views/content-views.module.d.ts.map +1 -0
  80. package/dist/lib/views/content-views.module.js +63 -0
  81. package/dist/lib/views/domain/errors/index.d.ts +6 -0
  82. package/dist/lib/views/domain/errors/index.d.ts.map +1 -0
  83. package/dist/lib/views/domain/errors/index.js +12 -0
  84. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts +10 -0
  85. package/dist/lib/views/domain/errors/saved-view-forbidden.error.d.ts.map +1 -0
  86. package/dist/lib/views/domain/errors/saved-view-forbidden.error.js +16 -0
  87. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts +9 -0
  88. package/dist/lib/views/domain/errors/saved-view-limit-error.d.ts.map +1 -0
  89. package/dist/lib/views/domain/errors/saved-view-limit-error.js +16 -0
  90. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts +5 -0
  91. package/dist/lib/views/domain/errors/saved-view-name-taken.error.d.ts.map +1 -0
  92. package/dist/lib/views/domain/errors/saved-view-name-taken.error.js +11 -0
  93. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts +9 -0
  94. package/dist/lib/views/domain/errors/saved-view-not-found.error.d.ts.map +1 -0
  95. package/dist/lib/views/domain/errors/saved-view-not-found.error.js +15 -0
  96. package/dist/lib/views/domain/saved-view.d.ts +66 -0
  97. package/dist/lib/views/domain/saved-view.d.ts.map +1 -0
  98. package/dist/lib/views/domain/saved-view.js +18 -0
  99. package/dist/lib/views/domain/saved-view.repository.d.ts +77 -0
  100. package/dist/lib/views/domain/saved-view.repository.d.ts.map +1 -0
  101. package/dist/lib/views/domain/saved-view.repository.js +5 -0
  102. package/dist/lib/views/http/controllers/saved-views.controller.d.ts +45 -0
  103. package/dist/lib/views/http/controllers/saved-views.controller.d.ts.map +1 -0
  104. package/dist/lib/views/http/controllers/saved-views.controller.js +216 -0
  105. package/dist/lib/views/http/controllers/to-http-error.d.ts +10 -0
  106. package/dist/lib/views/http/controllers/to-http-error.d.ts.map +1 -0
  107. package/dist/lib/views/http/controllers/to-http-error.js +26 -0
  108. package/dist/lib/views/http/dto/save-view.dto.d.ts +54 -0
  109. package/dist/lib/views/http/dto/save-view.dto.d.ts.map +1 -0
  110. package/dist/lib/views/http/dto/save-view.dto.js +230 -0
  111. package/dist/lib/views/http/dto/view-payload.validator.d.ts +20 -0
  112. package/dist/lib/views/http/dto/view-payload.validator.d.ts.map +1 -0
  113. package/dist/lib/views/http/dto/view-payload.validator.js +52 -0
  114. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts +17 -0
  115. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.d.ts.map +1 -0
  116. package/dist/lib/views/infrastructure/persistence/drizzle-saved-view.repository.js +125 -0
  117. package/dist/lib/views/infrastructure/schema/external-refs.d.ts +68 -0
  118. package/dist/lib/views/infrastructure/schema/external-refs.d.ts.map +1 -0
  119. package/dist/lib/views/infrastructure/schema/external-refs.js +29 -0
  120. package/dist/lib/views/infrastructure/schema/index.d.ts +8 -0
  121. package/dist/lib/views/infrastructure/schema/index.d.ts.map +1 -0
  122. package/dist/lib/views/infrastructure/schema/index.js +13 -0
  123. package/dist/lib/views/infrastructure/schema/saved-views.d.ts +275 -0
  124. package/dist/lib/views/infrastructure/schema/saved-views.d.ts.map +1 -0
  125. package/dist/lib/views/infrastructure/schema/saved-views.js +87 -0
  126. package/dist/lib/views/views.constants.d.ts +26 -0
  127. package/dist/lib/views/views.constants.d.ts.map +1 -0
  128. package/dist/lib/views/views.constants.js +28 -0
  129. package/dist/lib/views/views.tokens.d.ts +17 -0
  130. package/dist/lib/views/views.tokens.d.ts.map +1 -0
  131. package/dist/lib/views/views.tokens.js +21 -0
  132. package/migrations/0000_saved_views.sql +29 -0
  133. package/migrations/meta/0000_snapshot.json +256 -0
  134. package/migrations/meta/_journal.json +13 -0
  135. package/package.json +13 -12
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntryMatchQuery = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const drizzle_orm_1 = require("drizzle-orm");
7
+ const database_1 = require("@orthacms/database");
8
+ const utils_server_1 = require("@orthacms/utils-server");
9
+ const entry_extension_1 = require("../../../extension/entry-extension");
10
+ const entry_filter_provider_1 = require("../../../extension/entry-filter-provider");
11
+ const entry_filter_surface_1 = require("./entry-filter-surface");
12
+ /**
13
+ * Answers "which entries of this type match this filter?" against the same
14
+ * surface, parser and translator the records list uses.
15
+ *
16
+ * It exists so a downstream plugin can reuse content's filter semantics
17
+ * without reaching for the database or rebuilding the surface: the filterable
18
+ * paths, the relation-hop budget, the workspace scoping of every relation
19
+ * subquery, and the 400 on a malformed tree are all inherited rather than
20
+ * re-implemented. `@orthacms/alarms-server` evaluates its rules through this,
21
+ * which is what makes "the rule I saved from the list means what the list
22
+ * showed me" true by construction rather than by review.
23
+ *
24
+ * Two deliberate differences from the list query:
25
+ *
26
+ * - **No locale scoping.** The entries extension's `listScope` narrows a list
27
+ * to the active locale; a rule is a statement about the collection, so it
28
+ * must see every translation. The extension's *virtual filter fields* are
29
+ * still wired, so a rule may filter on them explicitly.
30
+ * - **Trashed rows are excluded, always.** There is no trash view here, and a
31
+ * tombstoned row is not something anyone can act on.
32
+ */
33
+ let EntryMatchQuery = class EntryMatchQuery {
34
+ db;
35
+ filterProviders;
36
+ extension;
37
+ constructor(db,
38
+ // Same pair as `EntriesService`: an alarm rule stores the records
39
+ // list's filter verbatim, so it has to parse against exactly the
40
+ // surface that list offers — including the fields other plugins add.
41
+ filterProviders, extension) {
42
+ this.db = db;
43
+ this.filterProviders = filterProviders;
44
+ this.extension = extension;
45
+ }
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, filter, workspaceId) {
54
+ const { schema } = (0, entry_filter_surface_1.buildEntryFilterSurface)(type, {
55
+ workspaceId,
56
+ extensionFields: this.filterProviders.compose(type, this.extension?.filterExtension(type))?.fields
57
+ });
58
+ (0, utils_server_1.parseFilterTree)(filter, schema);
59
+ }
60
+ /**
61
+ * Ids of `type` in `workspaceId` matching `filter`, oldest-id first so a
62
+ * batched scan is stable across calls.
63
+ */
64
+ async matchingIds(type, filter, workspaceId, options = {}) {
65
+ // An empty restriction means "none of them", not "all of them" — an
66
+ // `inArray(col, [])` would be a false predicate anyway, but returning
67
+ // early keeps a caller that batched its ids from paying for a query
68
+ // whose answer is known.
69
+ if (options.entryIds && options.entryIds.length === 0)
70
+ return [];
71
+ const where = await this.predicate(type, filter, workspaceId, options);
72
+ const table = type.table;
73
+ const rows = await this.db
74
+ .select({ id: table['id'] })
75
+ .from(type.table)
76
+ .where(where)
77
+ .orderBy(table['id'])
78
+ .limit(options.limit ?? Number.MAX_SAFE_INTEGER)
79
+ .offset(options.offset ?? 0);
80
+ return rows.map((row) => String(row.id));
81
+ }
82
+ /** How many entries of `type` match `filter` right now. */
83
+ async countMatching(type, filter, workspaceId) {
84
+ const where = await this.predicate(type, filter, workspaceId, {});
85
+ const [row] = await this.db
86
+ .select({ total: (0, drizzle_orm_1.count)() })
87
+ .from(type.table)
88
+ .where(where);
89
+ return Number(row?.total ?? 0);
90
+ }
91
+ /**
92
+ * How many entries of `type` the workspace holds at all — the denominator
93
+ * the rule editor shows beside a match count ("14 of 312"), which is what
94
+ * turns a number into a judgement about whether the rule is too broad.
95
+ */
96
+ async countAll(type, workspaceId) {
97
+ const [row] = await this.db
98
+ .select({ total: (0, drizzle_orm_1.count)() })
99
+ .from(type.table)
100
+ .where(this.scope(type, workspaceId));
101
+ return Number(row?.total ?? 0);
102
+ }
103
+ /**
104
+ * The workspace an entry belongs to, or `null` when no such row exists.
105
+ *
106
+ * A domain event names an entry and its content type but not its workspace
107
+ * — every content table carries its own `workspace_id`, so there is nothing
108
+ * for the envelope to add that a one-column lookup cannot answer. Exposed
109
+ * here rather than left to each subscriber, so a downstream plugin never
110
+ * has a reason to query a `content_<name>` table directly.
111
+ */
112
+ async workspaceOf(type, entryId) {
113
+ const table = type.table;
114
+ const [row] = await this.db
115
+ .select({ workspaceId: table['workspaceId'] })
116
+ .from(type.table)
117
+ .where((0, drizzle_orm_1.eq)(table['id'], entryId))
118
+ .limit(1);
119
+ return row?.workspaceId ? String(row.workspaceId) : null;
120
+ }
121
+ /** The workspace + not-trashed guard every scan starts from. */
122
+ scope(type, workspaceId) {
123
+ const table = type.table;
124
+ return (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(table['workspaceId'], workspaceId), type.paranoid ? (0, drizzle_orm_1.isNull)(table['deletedAt']) : undefined);
125
+ }
126
+ /** Scope + translated filter + optional id restriction. */
127
+ async predicate(type, filter, workspaceId, options) {
128
+ const filterExtension = this.filterProviders.compose(type, this.extension?.filterExtension(type));
129
+ const { schema } = (0, entry_filter_surface_1.buildEntryFilterSurface)(type, {
130
+ workspaceId,
131
+ extensionFields: filterExtension?.fields
132
+ });
133
+ const tree = (0, utils_server_1.parseFilterTree)(filter, schema);
134
+ const filterSql = await (0, utils_server_1.applyFilterTree)(tree, schema, type.table, this.db, filterExtension
135
+ ? {
136
+ resolveExtension: (rule) => filterExtension.resolve(rule, { type, workspaceId })
137
+ }
138
+ : {});
139
+ const table = type.table;
140
+ return (0, drizzle_orm_1.and)(this.scope(type, workspaceId), filterSql, options.entryIds
141
+ ? (0, drizzle_orm_1.inArray)(table['id'], [...options.entryIds])
142
+ : undefined);
143
+ }
144
+ };
145
+ exports.EntryMatchQuery = EntryMatchQuery;
146
+ exports.EntryMatchQuery = EntryMatchQuery = tslib_1.__decorate([
147
+ (0, common_1.Injectable)(),
148
+ tslib_1.__param(0, (0, database_1.InjectDatabase)()),
149
+ tslib_1.__param(2, (0, common_1.Optional)()),
150
+ tslib_1.__param(2, (0, common_1.Inject)(entry_extension_1.CONTENT_ENTRY_EXTENSION)),
151
+ tslib_1.__metadata("design:paramtypes", [Object, entry_filter_provider_1.EntryFilterProviderRegistry, Object])
152
+ ], EntryMatchQuery);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The **entry-access description port** — the seam a downstream plugin uses to
3
+ * tell a protocol adapter *that* an entry it just served is restricted, without
4
+ * this package knowing what a restriction is.
5
+ *
6
+ * It is the read scope's opposite number and deliberately not the same thing.
7
+ * `CONTENT_READ_SCOPE` decides which rows a reader gets; this describes the rows
8
+ * they got. A scope can only subtract, so by the time an annotation is asked
9
+ * for, the entry has already been admitted — which is why nothing here can
10
+ * refuse anything, and why an implementation that returned nothing would be a
11
+ * missing label rather than a leak.
12
+ *
13
+ * ## What it is for
14
+ *
15
+ * One thing, and it is worth being blunt about it: **a restricted entry must not
16
+ * be cached as public.** A CDN, a shared HTTP cache or a client store that keeps
17
+ * one reader's copy and hands it to the next turns a working entitlement into a
18
+ * data leak, and nothing else in the response says the entry was reader-scoped —
19
+ * it looks exactly like an open one. Everything else an implementation could
20
+ * report (who exactly may read it, which segments) is a question for the admin
21
+ * API, where the caller is a member of the workspace rather than a consumer of
22
+ * its content.
23
+ *
24
+ * ## Why a registry rather than a DI token
25
+ *
26
+ * The same reason the read scope uses one: Nest has **no multi-provider**, so
27
+ * two dynamic modules binding one token do not merge — the second silently
28
+ * replaces the first. Here that would mean an entry that one plugin knows is
29
+ * restricted reported as open because a second plugin was registered after it,
30
+ * which is precisely the mislabelling this port exists to prevent. Registration
31
+ * is a runtime `register(...)` call, the same shape as
32
+ * {@link contentReadScopeRegistrar}.
33
+ *
34
+ * ## The batching constraint
35
+ *
36
+ * `describe` takes the whole page of entry ids, not one. A protocol adapter
37
+ * resolves a list and then labels it; asking per entry would be a query per row
38
+ * on a path whose cost is otherwise one page read. An implementation that cannot
39
+ * answer for an id simply leaves it out of the map, and the caller reads a
40
+ * missing key as unrestricted — the same default absence carries everywhere
41
+ * else in this feature.
42
+ */
43
+ import { type Provider, type Type } from '@nestjs/common';
44
+ /** What one entry's restriction looks like from outside the plugin that owns it. */
45
+ export interface EntryAccessDescription {
46
+ /**
47
+ * Whether anything narrowed who may read this entry.
48
+ *
49
+ * `true` means the response is reader-specific and must not be stored in a
50
+ * shared cache without varying on whatever identifies the reader.
51
+ */
52
+ readonly restricted: boolean;
53
+ /**
54
+ * The axes that took part, as opaque keys (`org`, `plan`). A hint for a
55
+ * client deciding what to vary its own cache on — never the segments
56
+ * themselves, which would tell a consumer who *else* may read the entry.
57
+ */
58
+ readonly dimensions: readonly string[];
59
+ }
60
+ /** What a source is told about the page it is describing. */
61
+ export interface EntryAccessQuery {
62
+ /** The workspace the entries were read in. */
63
+ readonly workspaceId: string;
64
+ /** The ids to describe. Never empty — the caller skips an empty page. */
65
+ readonly entryIds: readonly string[];
66
+ }
67
+ /**
68
+ * One contributed description of what was served.
69
+ *
70
+ * Returning an empty map means "nothing to say about this page", which is what
71
+ * an installed-but-unconfigured plugin returns and what every entry defaults to.
72
+ */
73
+ export interface EntryAccessSource {
74
+ /** Describes the given entries, keyed by entry id. Ids may be omitted. */
75
+ describe(query: EntryAccessQuery): Promise<ReadonlyMap<string, EntryAccessDescription>>;
76
+ }
77
+ /**
78
+ * Every registered source, merged.
79
+ *
80
+ * The merge is a **union that can only tighten**: an entry is restricted if any
81
+ * source says so, and the dimensions are the union of what each reported. Two
82
+ * plugins narrowing reads for different reasons both need to be heard, and the
83
+ * safe direction for a disagreement is the one that keeps a shared cache from
84
+ * storing the entry.
85
+ */
86
+ export declare class EntryAccessSourceRegistry {
87
+ private readonly sources;
88
+ /** Adds a source. Registering the same instance twice is a no-op. */
89
+ register(source: EntryAccessSource): void;
90
+ /** Whether anything is registered — the cheap check before building a page. */
91
+ get active(): boolean;
92
+ /**
93
+ * Describes one page of entries.
94
+ *
95
+ * Returns an empty map when nothing is registered or the page is empty, so
96
+ * a caller on an installation with no scoping plugin does no work and every
97
+ * entry reads as unrestricted.
98
+ */
99
+ describe(query: EntryAccessQuery): Promise<ReadonlyMap<string, EntryAccessDescription>>;
100
+ }
101
+ /**
102
+ * Builds the DI provider that registers `sources` with content's access-source
103
+ * registry at bootstrap.
104
+ *
105
+ * A factory with an explicit `inject` list rather than a class with reflected
106
+ * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
107
+ * dependency typed as `Foo | null` emits `Object` for `design:paramtypes`, Nest
108
+ * injects `undefined` with no error, and the plugin ends up registering
109
+ * nothing.
110
+ */
111
+ export declare function entryAccessSourceRegistrar(label: string, ...sources: Type<EntryAccessSource>[]): Provider;
112
+ //# sourceMappingURL=entry-access-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-access-source.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/entry-access-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AAExB,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,QAAQ,CACJ,KAAK,EAAE,gBAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;;;GAQG;AACH,qBACa,yBAAyB;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAMzC,+EAA+E;IAC/E,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;;;OAMG;IACG,QAAQ,CACV,KAAK,EAAE,gBAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CAgC1D;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACtC,KAAK,EAAE,MAAM,EACb,GAAG,OAAO,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAAE,GACtC,QAAQ,CAYV"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The **virtual filter-field registry** — how more than one plugin contributes
3
+ * fields to a content type's `?filter=` surface.
4
+ *
5
+ * `CONTENT_ENTRY_EXTENSION.filterExtension` already declares virtual fields
6
+ * (i18n's `hasLocale` / `missingLocale` / `localeCount`), but that port is a
7
+ * documented **single binding**: one provider per app, and i18n holds it. Nest
8
+ * has no multi-provider, so a second plugin binding the token would silently
9
+ * replace the first — for a filter surface that means fields quietly vanishing
10
+ * from the picker and saved filters starting to 400.
11
+ *
12
+ * So a plugin registers here instead (`@orthacms/segments-server` contributes
13
+ * "can be seen by" / "cannot be seen by" / "restricted"), the same shape and the
14
+ * same reason as `contentReadScopeRegistrar`. The bound extension keeps its
15
+ * method; {@link EntryFilterProviderRegistry.compose} folds the two together
16
+ * into the one `EntryFilterExtension` the query path already knows how to use,
17
+ * so nothing downstream changed.
18
+ *
19
+ * ## What a contribution owes
20
+ *
21
+ * **Every emitted subquery MUST scope to the workspace it is handed.** A virtual
22
+ * field is a subquery over a table this package knows nothing about; one that
23
+ * forgets the workspace turns a filter into a cross-tenant read.
24
+ *
25
+ * **Only declare fields you can answer, with only the operators you support.**
26
+ * The declared `fields` become the SQL whitelist, so an operator the resolver
27
+ * refuses reaches the user as "couldn't load this collection" over a rule the
28
+ * picker itself proposed. Narrow the operator set in the admin's `FilterField`
29
+ * to match.
30
+ *
31
+ * **A filter narrows what is listed; it is not a visibility rule.** Reachability
32
+ * is `CONTENT_READ_SCOPE`'s job and is applied separately. A field here must not
33
+ * be relied on to hide anything.
34
+ */
35
+ import { type Provider, type Type } from '@nestjs/common';
36
+ import type { AnyContentType } from '../types/content-type';
37
+ import type { EntryFilterExtension } from './entry-extension';
38
+ /** One plugin's virtual filter fields. */
39
+ export interface EntryFilterProvider {
40
+ /**
41
+ * The fields this plugin adds to `type`'s filter surface, or `undefined`
42
+ * when it adds none — which is what a plugin that is installed but
43
+ * unconfigured must return, so a bare install pays nothing.
44
+ */
45
+ filterFor(type: AnyContentType): EntryFilterExtension | undefined;
46
+ }
47
+ /** Every registered provider, in registration order. */
48
+ export declare class EntryFilterProviderRegistry {
49
+ private readonly logger;
50
+ private readonly providers;
51
+ /** Adds a provider. Registering the same instance twice is a no-op. */
52
+ register(provider: EntryFilterProvider): void;
53
+ /**
54
+ * Fold the bound extension's contribution and every registered provider's
55
+ * into one {@link EntryFilterExtension}.
56
+ *
57
+ * Returns `undefined` when nobody contributed anything, so a type with no
58
+ * virtual fields builds exactly the surface it did before this registry
59
+ * existed — and `entries.service` keeps its "no extension, no
60
+ * `resolveExtension`" branch.
61
+ *
62
+ * `resolve` routes by the rule's **field name**, to whoever declared it.
63
+ * A name declared twice is kept by its first declarer and the duplicate is
64
+ * dropped with a warning: silently letting the last writer win would make
65
+ * the meaning of a saved filter depend on plugin registration order.
66
+ */
67
+ compose(type: AnyContentType, bound: EntryFilterExtension | undefined): EntryFilterExtension | undefined;
68
+ }
69
+ /**
70
+ * Builds the DI provider that registers `providers` with content's filter-field
71
+ * registry at bootstrap.
72
+ *
73
+ * A factory with an explicit `inject` list rather than a class with reflected
74
+ * parameters, for the reason `contentReadScopeRegistrar` documents: an optional
75
+ * dependency typed `Foo | null` emits `Object` for `design:paramtypes`, Nest
76
+ * injects `undefined` with no error, and the plugin registers nothing.
77
+ */
78
+ export declare function entryFilterProviderRegistrar(label: string, ...providers: Type<EntryFilterProvider>[]): Provider;
79
+ //# sourceMappingURL=entry-filter-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-filter-provider.d.ts","sourceRoot":"","sources":["../../../src/lib/extension/entry-filter-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAIH,KAAK,QAAQ,EACb,KAAK,IAAI,EACZ,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAChC;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,GAAG,SAAS,CAAC;CACrE;AAED,wDAAwD;AACxD,qBACa,2BAA2B;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgD;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IAEvD,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAM7C;;;;;;;;;;;;;OAaG;IACH,OAAO,CACH,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,oBAAoB,GAAG,SAAS,GACxC,oBAAoB,GAAG,SAAS;CAyCtC;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CACxC,KAAK,EAAE,MAAM,EACb,GAAG,SAAS,EAAE,IAAI,CAAC,mBAAmB,CAAC,EAAE,GAC1C,QAAQ,CAYV"}
@@ -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
+ }