@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
|
@@ -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"}
|
|
@@ -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,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"}
|