@revealui/core 0.2.0 → 0.3.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/README.md +137 -30
- package/dist/api/compression.d.ts.map +1 -1
- package/dist/api/payload-optimization.d.ts.map +1 -1
- package/dist/api/rate-limit.d.ts +30 -29
- package/dist/api/rate-limit.d.ts.map +1 -1
- package/dist/api/rate-limit.js +79 -4
- package/dist/api/response-cache.d.ts.map +1 -1
- package/dist/api/response-cache.js +1 -1
- package/dist/api/rest.d.ts.map +1 -1
- package/dist/api/rest.js +5 -4
- package/dist/auth/access.d.ts.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/cache/query-cache.d.ts +12 -10
- package/dist/cache/query-cache.d.ts.map +1 -1
- package/dist/cache/query-cache.js +38 -42
- package/dist/caching/app-cache.d.ts +5 -0
- package/dist/caching/app-cache.d.ts.map +1 -1
- package/dist/caching/app-cache.js +9 -1
- package/dist/caching/cdn-config.d.ts +2 -2
- package/dist/caching/cdn-config.d.ts.map +1 -1
- package/dist/caching/cdn-config.js +5 -15
- package/dist/caching/edge-cache.d.ts +1 -1
- package/dist/caching/edge-cache.d.ts.map +1 -1
- package/dist/caching/edge-cache.js +44 -11
- package/dist/caching/index.d.ts +6 -0
- package/dist/caching/index.d.ts.map +1 -0
- package/dist/caching/index.js +5 -0
- package/dist/caching/service-worker.d.ts +10 -18
- package/dist/caching/service-worker.d.ts.map +1 -1
- package/dist/caching/service-worker.js +5 -4
- package/dist/client/admin/RichText.d.ts +1 -1
- package/dist/client/admin/RichText.d.ts.map +1 -1
- package/dist/client/admin/components/AdminDashboard.d.ts.map +1 -1
- package/dist/client/admin/components/AdminDashboard.js +178 -205
- package/dist/client/admin/components/CollectionList.d.ts.map +1 -1
- package/dist/client/admin/components/DocumentForm.d.ts.map +1 -1
- package/dist/client/admin/components/DocumentForm.js +130 -6
- package/dist/client/admin/components/GlobalForm.d.ts.map +1 -1
- package/dist/client/admin/context/ServerFunctionContext.d.ts +8 -0
- package/dist/client/admin/context/ServerFunctionContext.d.ts.map +1 -0
- package/dist/client/admin/context/ServerFunctionContext.js +15 -0
- package/dist/client/admin/i18n/en.d.ts.map +1 -1
- package/dist/client/admin/index.d.ts +1 -0
- package/dist/client/admin/index.d.ts.map +1 -1
- package/dist/client/admin/index.js +1 -0
- package/dist/client/admin/layout.d.ts +1 -1
- package/dist/client/admin/layout.d.ts.map +1 -1
- package/dist/client/admin/layout.js +3 -2
- package/dist/client/admin/page.d.ts.map +1 -1
- package/dist/client/admin/utils/apiClient.d.ts.map +1 -1
- package/dist/client/admin/utils/apiClient.js +0 -4
- package/dist/client/admin/utils/auth.d.ts +0 -4
- package/dist/client/admin/utils/auth.d.ts.map +1 -1
- package/dist/client/admin/utils/auth.js +0 -6
- package/dist/client/admin/utils/index.d.ts +0 -1
- package/dist/client/admin/utils/index.d.ts.map +1 -1
- package/dist/client/admin/utils/index.js +0 -1
- package/dist/client/admin/utils/serializeConfig.d.ts.map +1 -1
- package/dist/client/hooks.d.ts.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +0 -2
- package/dist/client/richtext/RichTextEditor.d.ts.map +1 -1
- package/dist/client/richtext/components/ImageNodeComponent.d.ts.map +1 -1
- package/dist/client/richtext/components/ImageNodeComponent.js +0 -1
- package/dist/client/richtext/components/ImageUploadButton.d.ts +2 -0
- package/dist/client/richtext/components/ImageUploadButton.d.ts.map +1 -1
- package/dist/client/richtext/components/ImageUploadButton.js +30 -15
- package/dist/client/richtext/index.d.ts.map +1 -1
- package/dist/client/richtext/nodes/DecoratorBlockNode.d.ts.map +1 -1
- package/dist/client/richtext/nodes/ImageNode.d.ts.map +1 -1
- package/dist/client/richtext/plugins/CollaborationPlugin.d.ts.map +1 -1
- package/dist/client/richtext/plugins/CursorsOverlayPlugin.d.ts.map +1 -1
- package/dist/client/richtext/plugins/FloatingToolbarPlugin.d.ts.map +1 -1
- package/dist/client/richtext/plugins/ImagePlugin.d.ts.map +1 -1
- package/dist/client/richtext/plugins/ToolbarPlugin.d.ts.map +1 -1
- package/dist/client/ui/index.d.ts.map +1 -1
- package/dist/client/ui/index.js +1 -1
- package/dist/collections/CollectionOperations.d.ts +7 -7
- package/dist/collections/CollectionOperations.d.ts.map +1 -1
- package/dist/collections/CollectionOperations.js +15 -1
- package/dist/collections/hooks.d.ts.map +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/operations/create.d.ts +2 -4
- package/dist/collections/operations/create.d.ts.map +1 -1
- package/dist/collections/operations/create.js +9 -7
- package/dist/collections/operations/createMany.d.ts +12 -0
- package/dist/collections/operations/createMany.d.ts.map +1 -0
- package/dist/collections/operations/createMany.js +43 -0
- package/dist/collections/operations/delete.d.ts +1 -1
- package/dist/collections/operations/delete.d.ts.map +1 -1
- package/dist/collections/operations/delete.js +31 -2
- package/dist/collections/operations/deleteMany.d.ts +11 -0
- package/dist/collections/operations/deleteMany.d.ts.map +1 -0
- package/dist/collections/operations/deleteMany.js +50 -0
- package/dist/collections/operations/fieldHooks.d.ts +2 -2
- package/dist/collections/operations/fieldHooks.d.ts.map +1 -1
- package/dist/collections/operations/fieldHooks.js +4 -4
- package/dist/collections/operations/find.d.ts +2 -4
- package/dist/collections/operations/find.d.ts.map +1 -1
- package/dist/collections/operations/find.js +115 -8
- package/dist/collections/operations/findById.d.ts +3 -4
- package/dist/collections/operations/findById.d.ts.map +1 -1
- package/dist/collections/operations/findById.js +53 -1
- package/dist/collections/operations/sqlAdapter.d.ts +23 -0
- package/dist/collections/operations/sqlAdapter.d.ts.map +1 -0
- package/dist/collections/operations/sqlAdapter.js +76 -0
- package/dist/collections/operations/update.d.ts +3 -5
- package/dist/collections/operations/update.d.ts.map +1 -1
- package/dist/collections/operations/update.js +103 -11
- package/dist/collections/operations/updateMany.d.ts +11 -0
- package/dist/collections/operations/updateMany.d.ts.map +1 -0
- package/dist/collections/operations/updateMany.js +52 -0
- package/dist/collections/registry.d.ts +12 -0
- package/dist/collections/registry.d.ts.map +1 -0
- package/dist/collections/registry.js +38 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/utils.d.ts +0 -10
- package/dist/config/utils.d.ts.map +1 -1
- package/dist/config/utils.js +18 -17
- package/dist/database/index.d.ts +3 -0
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +1 -5
- package/dist/database/safe-parse.d.ts +26 -0
- package/dist/database/safe-parse.d.ts.map +1 -0
- package/dist/database/safe-parse.js +42 -0
- package/dist/database/ssl-config.d.ts.map +1 -1
- package/dist/database/type-adapter.d.ts.map +1 -1
- package/dist/database/universal-postgres.d.ts.map +1 -1
- package/dist/database/universal-postgres.js +18 -13
- package/dist/dataloader.d.ts.map +1 -1
- package/dist/dataloader.js +16 -2
- package/dist/error-handling/circuit-breaker.d.ts +1 -1
- package/dist/error-handling/circuit-breaker.d.ts.map +1 -1
- package/dist/error-handling/circuit-breaker.js +11 -3
- package/dist/error-handling/error-boundary.d.ts.map +1 -1
- package/dist/error-handling/error-reporter.d.ts +6 -5
- package/dist/error-handling/error-reporter.d.ts.map +1 -1
- package/dist/error-handling/error-reporter.js +26 -41
- package/dist/error-handling/fallback-components.d.ts.map +1 -1
- package/dist/error-handling/fallback-components.js +1 -1
- package/dist/error-handling/index.d.ts +3 -5
- package/dist/error-handling/index.d.ts.map +1 -1
- package/dist/error-handling/index.js +2 -5
- package/dist/error-handling/retry.d.ts.map +1 -1
- package/dist/error-handling/retry.js +13 -8
- package/dist/factories/builders.d.ts.map +1 -1
- package/dist/factories/index.d.ts.map +1 -1
- package/dist/features.d.ts +5 -5
- package/dist/features.d.ts.map +1 -1
- package/dist/features.js +6 -5
- package/dist/fieldTraversal.d.ts.map +1 -1
- package/dist/fields/config/types.d.ts.map +1 -1
- package/dist/fields/getDefaultValue.d.ts.map +1 -1
- package/dist/fields/getFieldPaths.d.ts.map +1 -1
- package/dist/fields/hooks/afterRead/index.d.ts.map +1 -1
- package/dist/fields/hooks/afterRead/promise.d.ts.map +1 -1
- package/dist/fields/hooks/afterRead/traverseFields.d.ts.map +1 -1
- package/dist/generated/types/cms.d.ts.map +1 -1
- package/dist/generated/types/cms.js +0 -1
- package/dist/generated/types/index.d.ts +0 -3
- package/dist/generated/types/index.d.ts.map +1 -1
- package/dist/generated/types/index.js +0 -7
- package/dist/generated/types/neon.d.ts.map +1 -1
- package/dist/generated/types/neon.js +4 -2
- package/dist/globals/GlobalOperations.d.ts.map +1 -1
- package/dist/globals/GlobalOperations.js +4 -2
- package/dist/globals/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -6
- package/dist/instance/RevealUIInstance.d.ts.map +1 -1
- package/dist/instance/RevealUIInstance.js +50 -69
- package/dist/instance/index.d.ts.map +1 -1
- package/dist/instance/logger.d.ts.map +1 -1
- package/dist/instance/methods/create.d.ts.map +1 -1
- package/dist/instance/methods/create.js +4 -4
- package/dist/instance/methods/delete.d.ts.map +1 -1
- package/dist/instance/methods/delete.js +5 -5
- package/dist/instance/methods/find.d.ts.map +1 -1
- package/dist/instance/methods/find.js +0 -3
- package/dist/instance/methods/findById.d.ts.map +1 -1
- package/dist/instance/methods/findById.js +0 -3
- package/dist/instance/methods/hooks.d.ts.map +1 -1
- package/dist/instance/methods/hooks.js +3 -1
- package/dist/instance/methods/update.d.ts.map +1 -1
- package/dist/instance/methods/update.js +4 -4
- package/dist/jobs/index.d.ts +16 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +14 -0
- package/dist/jobs/queue.d.ts +57 -0
- package/dist/jobs/queue.d.ts.map +1 -0
- package/dist/jobs/queue.js +134 -0
- package/dist/license-encryption.d.ts +21 -0
- package/dist/license-encryption.d.ts.map +1 -0
- package/dist/license-encryption.js +74 -0
- package/dist/license.d.ts +33 -7
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +119 -16
- package/dist/monitoring/alerts.d.ts.map +1 -1
- package/dist/monitoring/cleanup-manager.d.ts.map +1 -1
- package/dist/monitoring/health-monitor.d.ts.map +1 -1
- package/dist/monitoring/index.d.ts.map +1 -1
- package/dist/monitoring/process-registry.d.ts.map +1 -1
- package/dist/monitoring/query-monitor.d.ts.map +1 -1
- package/dist/monitoring/types.d.ts.map +1 -1
- package/dist/monitoring/zombie-detector.d.ts.map +1 -1
- package/dist/monitoring/zombie-detector.js +5 -0
- package/dist/nextjs/index.d.ts.map +1 -1
- package/dist/nextjs/utilities.d.ts.map +1 -1
- package/dist/nextjs/withRevealUI.d.ts.map +1 -1
- package/dist/observability/alerts.d.ts.map +1 -1
- package/dist/observability/alerts.js +1 -2
- package/dist/observability/health-check.d.ts +1 -5
- package/dist/observability/health-check.d.ts.map +1 -1
- package/dist/observability/health-check.js +37 -43
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/logger.d.ts.map +1 -1
- package/dist/observability/logger.js +1 -1
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/tracing.d.ts.map +1 -1
- package/dist/observability/tracing.js +0 -1
- package/dist/optimization/asset-optimizer.d.ts +6 -2
- package/dist/optimization/asset-optimizer.d.ts.map +1 -1
- package/dist/optimization/asset-optimizer.js +31 -7
- package/dist/optimization/bundle-analyzer.d.ts +1 -1
- package/dist/optimization/bundle-analyzer.d.ts.map +1 -1
- package/dist/optimization/bundle-analyzer.js +29 -5
- package/dist/optimization/code-splitting.d.ts +0 -23
- package/dist/optimization/code-splitting.d.ts.map +1 -1
- package/dist/optimization/code-splitting.js +0 -29
- package/dist/plugins/form-builder.d.ts.map +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/nested-docs.d.ts +4 -0
- package/dist/plugins/nested-docs.d.ts.map +1 -1
- package/dist/plugins/nested-docs.js +50 -5
- package/dist/plugins/redirects.d.ts.map +1 -1
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/queryBuilder.d.ts.map +1 -1
- package/dist/queries/queryBuilder.js +15 -5
- package/dist/relationships/analyzer.d.ts.map +1 -1
- package/dist/relationships/analyzer.js +8 -0
- package/dist/relationships/index.d.ts.map +1 -1
- package/dist/relationships/populate-core.d.ts +57 -0
- package/dist/relationships/populate-core.d.ts.map +1 -0
- package/dist/relationships/populate-core.js +116 -0
- package/dist/relationships/populate-helpers.d.ts +5 -51
- package/dist/relationships/populate-helpers.d.ts.map +1 -1
- package/dist/relationships/populate-helpers.js +4 -109
- package/dist/relationships/population.d.ts +1 -9
- package/dist/relationships/population.d.ts.map +1 -1
- package/dist/relationships/population.js +8 -3
- package/dist/revealui.d.ts.map +1 -1
- package/dist/richtext/exports/client/rcc.d.ts.map +1 -1
- package/dist/richtext/exports/client/rcc.js +1 -1
- package/dist/richtext/exports/server/rsc.d.ts +17 -0
- package/dist/richtext/exports/server/rsc.d.ts.map +1 -1
- package/dist/richtext/exports/server/rsc.js +61 -5
- package/dist/richtext/index.d.ts.map +1 -1
- package/dist/richtext/lexical.d.ts.map +1 -1
- package/dist/security/audit.d.ts +1 -1
- package/dist/security/audit.d.ts.map +1 -1
- package/dist/security/audit.js +4 -2
- package/dist/security/auth.d.ts +29 -160
- package/dist/security/auth.d.ts.map +1 -1
- package/dist/security/auth.js +150 -367
- package/dist/security/authorization.d.ts +7 -31
- package/dist/security/authorization.d.ts.map +1 -1
- package/dist/security/authorization.js +72 -14
- package/dist/security/encryption.d.ts +56 -44
- package/dist/security/encryption.d.ts.map +1 -1
- package/dist/security/encryption.js +128 -100
- package/dist/security/gdpr-storage.d.ts +102 -0
- package/dist/security/gdpr-storage.d.ts.map +1 -0
- package/dist/security/gdpr-storage.js +65 -0
- package/dist/security/gdpr.d.ts +57 -37
- package/dist/security/gdpr.d.ts.map +1 -1
- package/dist/security/gdpr.js +155 -94
- package/dist/security/headers.d.ts +4 -2
- package/dist/security/headers.d.ts.map +1 -1
- package/dist/security/headers.js +35 -17
- package/dist/security/index.d.ts +3 -16
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +3 -16
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/renderPage.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -4
- package/dist/storage/vercel-blob.d.ts.map +1 -1
- package/dist/translations/index.d.ts.map +1 -1
- package/dist/types/access.d.ts.map +1 -1
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/cms.d.ts.map +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/extensions.d.ts.map +1 -1
- package/dist/types/frontend.d.ts.map +1 -1
- package/dist/types/generated.d.ts +0 -2
- package/dist/types/generated.d.ts.map +1 -1
- package/dist/types/generated.js +0 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/interfaces/app.d.ts.map +1 -1
- package/dist/types/jobs.d.ts.map +1 -1
- package/dist/types/legacy.d.ts.map +1 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/types/query.d.ts.map +1 -1
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/richtext.d.ts.map +1 -1
- package/dist/types/runtime.d.ts +59 -1
- package/dist/types/runtime.d.ts.map +1 -1
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/user.d.ts.map +1 -1
- package/dist/utils/access-conversion.d.ts.map +1 -1
- package/dist/utils/api-wrapper.d.ts.map +1 -1
- package/dist/utils/api-wrapper.js +1 -1
- package/dist/utils/block-conversion.d.ts.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/deep-clone.js +0 -1
- package/dist/utils/error-responses.d.ts.map +1 -1
- package/dist/utils/errors.d.ts +36 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +103 -0
- package/dist/utils/field-conversion.d.ts +1 -1
- package/dist/utils/field-conversion.d.ts.map +1 -1
- package/dist/utils/flattenResult.d.ts.map +1 -1
- package/dist/utils/flattenResult.js +0 -1
- package/dist/utils/getBlockSelect.d.ts.map +1 -1
- package/dist/utils/getSelectMode.d.ts.map +1 -1
- package/dist/utils/isValidID.d.ts.map +1 -1
- package/dist/utils/json-parsing.d.ts.map +1 -1
- package/dist/utils/logger-client.d.ts.map +1 -1
- package/dist/utils/logger-server.d.ts.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/request-context.d.ts.map +1 -1
- package/dist/utils/stripUnselectedFields.d.ts.map +1 -1
- package/dist/utils/type-guards.d.ts.map +1 -1
- package/package.json +39 -16
|
@@ -6,29 +6,138 @@
|
|
|
6
6
|
import { afterRead } from '../../fields/hooks/afterRead/index.js';
|
|
7
7
|
import { buildWhereClause } from '../../queries/queryBuilder.js';
|
|
8
8
|
import { deserializeJsonFields } from '../../utils/json-parsing.js';
|
|
9
|
+
import { countDocumentsQuery, escapeIdentifier, listDocumentsQuery } from './sqlAdapter.js';
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a collection's access.read function.
|
|
12
|
+
* Returns true (allow all), false (deny all), or a WhereClause to merge into the query.
|
|
13
|
+
*/
|
|
14
|
+
async function evaluateReadAccess(config, options) {
|
|
15
|
+
// If overrideAccess is explicitly set, skip access checks (caller already validated permission)
|
|
16
|
+
if (options.overrideAccess)
|
|
17
|
+
return true;
|
|
18
|
+
const accessConfig = config
|
|
19
|
+
.access;
|
|
20
|
+
const readAccess = accessConfig?.read;
|
|
21
|
+
// No access rule defined = allow all (backward compatible)
|
|
22
|
+
if (!readAccess)
|
|
23
|
+
return true;
|
|
24
|
+
const req = options.req;
|
|
25
|
+
if (!req)
|
|
26
|
+
return false; // No request context = deny (safe default)
|
|
27
|
+
const result = await readAccess({ req });
|
|
28
|
+
if (typeof result === 'boolean')
|
|
29
|
+
return result;
|
|
30
|
+
// WhereClause returned = row-level filtering (e.g. { author: { equals: user.id } })
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
9
33
|
export async function find(config, db, options) {
|
|
10
34
|
const { where, limit = 10, page = 1, sort, depth = 0, req, populate: populateOption } = options;
|
|
11
35
|
// Validate depth
|
|
12
36
|
if (depth < 0 || depth > 3) {
|
|
13
37
|
throw new Error(`Depth must be between 0 and 3, got ${depth}`);
|
|
14
38
|
}
|
|
39
|
+
// --- Access control enforcement ---
|
|
40
|
+
const accessResult = await evaluateReadAccess(config, options);
|
|
41
|
+
if (accessResult === false) {
|
|
42
|
+
// Deny: return empty result set (same shape as a normal response)
|
|
43
|
+
return {
|
|
44
|
+
docs: [],
|
|
45
|
+
totalDocs: 0,
|
|
46
|
+
limit,
|
|
47
|
+
totalPages: 0,
|
|
48
|
+
page,
|
|
49
|
+
pagingCounter: 0,
|
|
50
|
+
hasPrevPage: false,
|
|
51
|
+
hasNextPage: false,
|
|
52
|
+
prevPage: null,
|
|
53
|
+
nextPage: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Merge access-control WhereClause with user-provided where filter
|
|
57
|
+
const mergedWhere = accessResult === true ? where : where ? { and: [where, accessResult] } : accessResult;
|
|
58
|
+
// Replace where in options for downstream use.
|
|
59
|
+
// When access returned a WhereClause (not just boolean true), set overrideAccess: true
|
|
60
|
+
// to prevent infinite recursion if collectionStorage.find re-invokes this function.
|
|
61
|
+
const accessOptions = accessResult === true
|
|
62
|
+
? { ...options, where: mergedWhere }
|
|
63
|
+
: { ...options, where: mergedWhere, overrideAccess: true };
|
|
64
|
+
if (db?.collectionStorage?.find) {
|
|
65
|
+
const result = await db.collectionStorage.find(config, accessOptions);
|
|
66
|
+
if (result !== undefined) {
|
|
67
|
+
if (req && depth > 0) {
|
|
68
|
+
const sanitizedConfig = {
|
|
69
|
+
...config,
|
|
70
|
+
fields: config.fields,
|
|
71
|
+
flattenedFields: config.fields,
|
|
72
|
+
endpoints: config.endpoints === false ? undefined : config.endpoints,
|
|
73
|
+
};
|
|
74
|
+
const docs = await Promise.all(result.docs.map(async (doc) => {
|
|
75
|
+
return await afterRead({
|
|
76
|
+
collection: sanitizedConfig,
|
|
77
|
+
context: req.context || {},
|
|
78
|
+
currentDepth: 1,
|
|
79
|
+
depth,
|
|
80
|
+
doc,
|
|
81
|
+
draft: false,
|
|
82
|
+
fallbackLocale: req.fallbackLocale || 'en',
|
|
83
|
+
findMany: true,
|
|
84
|
+
flattenLocales: true,
|
|
85
|
+
global: null,
|
|
86
|
+
locale: req.locale || 'en',
|
|
87
|
+
overrideAccess: false,
|
|
88
|
+
populate: populateOption,
|
|
89
|
+
req,
|
|
90
|
+
select: undefined,
|
|
91
|
+
showHiddenFields: false,
|
|
92
|
+
});
|
|
93
|
+
}));
|
|
94
|
+
return {
|
|
95
|
+
...result,
|
|
96
|
+
docs,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
15
102
|
// Build query based on database adapter
|
|
16
103
|
if (db?.query) {
|
|
104
|
+
// Dynamic collection storage is quarantined in sqlAdapter.ts until this
|
|
105
|
+
// layer is redesigned around typed tables that Drizzle can model directly.
|
|
17
106
|
const offset = (page - 1) * limit;
|
|
18
107
|
const tableName = config.slug;
|
|
19
|
-
// Build WHERE clause using query builder
|
|
108
|
+
// Build WHERE clause using query builder (uses access-merged where)
|
|
20
109
|
const params = [];
|
|
21
|
-
const whereClause = buildWhereClause(
|
|
110
|
+
const whereClause = buildWhereClause(mergedWhere, params, {
|
|
22
111
|
parameterStyle: 'postgres',
|
|
23
112
|
includeWhereKeyword: false,
|
|
24
113
|
quoteFields: true,
|
|
25
114
|
});
|
|
26
|
-
// Build ORDER BY clause
|
|
115
|
+
// Build ORDER BY clause with field name validation
|
|
27
116
|
let orderByClause = '';
|
|
28
117
|
if (sort) {
|
|
118
|
+
// Build allowlist from collection fields + common system columns
|
|
119
|
+
const allowedFields = new Set([
|
|
120
|
+
'id',
|
|
121
|
+
'createdAt',
|
|
122
|
+
'updatedAt',
|
|
123
|
+
'created_at',
|
|
124
|
+
'updated_at',
|
|
125
|
+
'_json',
|
|
126
|
+
]);
|
|
127
|
+
if (config.fields) {
|
|
128
|
+
for (const field of config.fields) {
|
|
129
|
+
if (field.name)
|
|
130
|
+
allowedFields.add(field.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
29
133
|
const sortConditions = [];
|
|
30
134
|
Object.entries(sort).forEach(([key, direction]) => {
|
|
31
|
-
|
|
135
|
+
if (!allowedFields.has(key)) {
|
|
136
|
+
throw new Error(`Invalid sort field: "${key}". Must be a defined field on the "${tableName}" collection.`);
|
|
137
|
+
}
|
|
138
|
+
// Escape embedded double quotes in identifier to prevent SQL injection
|
|
139
|
+
const escaped = escapeIdentifier(key);
|
|
140
|
+
sortConditions.push(`"${escaped}" ${direction === '-1' ? 'DESC' : 'ASC'}`);
|
|
32
141
|
});
|
|
33
142
|
orderByClause = sortConditions.length > 0 ? `ORDER BY ${sortConditions.join(', ')}` : '';
|
|
34
143
|
}
|
|
@@ -38,9 +147,7 @@ export async function find(config, db, options) {
|
|
|
38
147
|
if (whereClause?.trim().toUpperCase().startsWith('WHERE')) {
|
|
39
148
|
throw new Error(`WHERE clause unexpectedly starts with "WHERE" keyword. This indicates a bug in buildWhereClause. Clause: ${whereClause}`);
|
|
40
149
|
}
|
|
41
|
-
const countQuery = whereClause
|
|
42
|
-
? `SELECT COUNT(*) as total FROM "${tableName}" WHERE ${whereClause}`
|
|
43
|
-
: `SELECT COUNT(*) as total FROM "${tableName}"`;
|
|
150
|
+
const countQuery = countDocumentsQuery(tableName, whereClause);
|
|
44
151
|
const countResult = await db.query(countQuery, params);
|
|
45
152
|
const totalDocs = Number(countResult.rows[0]?.total) || 0;
|
|
46
153
|
// Execute data query (PostgreSQL uses $1, $2 for parameters)
|
|
@@ -59,7 +166,7 @@ export async function find(config, db, options) {
|
|
|
59
166
|
}
|
|
60
167
|
const limitParam = params.length + 1;
|
|
61
168
|
const offsetParam = params.length + 2;
|
|
62
|
-
const dataQuery =
|
|
169
|
+
const dataQuery = listDocumentsQuery(tableName, whereClause, orderByClause, limitParam, offsetParam);
|
|
63
170
|
const docsResult = await db.query(dataQuery, [...params, limit, offset]);
|
|
64
171
|
let docs = docsResult.rows.map((row) => {
|
|
65
172
|
return deserializeJsonFields(row, tableName);
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Finds a single document by ID with optional relationship population.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
7
|
-
export declare function findByID(config: RevealCollectionConfig, db: {
|
|
8
|
-
query: (query: string, values?: unknown[]) => Promise<DatabaseResult>;
|
|
9
|
-
} | null, options: {
|
|
6
|
+
import type { PopulateType, QueryableDatabaseAdapter, RevealCollectionConfig, RevealDocument, RevealRequest } from '../../types/index.js';
|
|
7
|
+
export declare function findByID(config: RevealCollectionConfig, db: QueryableDatabaseAdapter | null, options: {
|
|
10
8
|
id: string | number;
|
|
11
9
|
depth?: number;
|
|
12
10
|
req?: RevealRequest;
|
|
13
11
|
populate?: PopulateType;
|
|
12
|
+
overrideAccess?: boolean;
|
|
14
13
|
}): Promise<RevealDocument | null>;
|
|
15
14
|
//# sourceMappingURL=findById.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findById.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/findById.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"findById.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/findById.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EACd,aAAa,EAEd,MAAM,sBAAsB,CAAC;AAI9B,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE;IACP,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GACA,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAwHhC"}
|
|
@@ -5,17 +5,69 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { afterRead } from '../../fields/hooks/afterRead/index.js';
|
|
7
7
|
import { deserializeJsonFields } from '../../utils/json-parsing.js';
|
|
8
|
+
import { selectByIdQuery } from './sqlAdapter.js';
|
|
8
9
|
export async function findByID(config, db, options) {
|
|
9
10
|
const { id, depth = 0, req, populate: populateOption } = options;
|
|
10
11
|
// Validate depth
|
|
11
12
|
if (depth < 0 || depth > 3) {
|
|
12
13
|
throw new Error(`Depth must be between 0 and 3, got ${depth}`);
|
|
13
14
|
}
|
|
15
|
+
// --- Access control enforcement ---
|
|
16
|
+
if (!options.overrideAccess) {
|
|
17
|
+
const accessConfig = config.access;
|
|
18
|
+
const readAccess = accessConfig?.read;
|
|
19
|
+
if (readAccess) {
|
|
20
|
+
if (!req)
|
|
21
|
+
return null; // No request context = deny
|
|
22
|
+
const result = await readAccess({ req, id });
|
|
23
|
+
if (result === false)
|
|
24
|
+
return null;
|
|
25
|
+
// If result is a WhereClause (row-level filter), we fetch the doc first then verify.
|
|
26
|
+
// For findByID, we check after fetch whether the doc matches the access filter.
|
|
27
|
+
// Boolean true = allow, WhereClause = post-fetch filter (handled below after query).
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (db?.collectionStorage?.findByID) {
|
|
31
|
+
const doc = await db.collectionStorage.findByID(config, { id });
|
|
32
|
+
if (doc !== undefined) {
|
|
33
|
+
if (!doc)
|
|
34
|
+
return null;
|
|
35
|
+
if (req && depth > 0) {
|
|
36
|
+
const sanitizedConfig = {
|
|
37
|
+
...config,
|
|
38
|
+
fields: config.fields,
|
|
39
|
+
flattenedFields: config.fields,
|
|
40
|
+
endpoints: config.endpoints === false ? undefined : config.endpoints,
|
|
41
|
+
};
|
|
42
|
+
return await afterRead({
|
|
43
|
+
collection: sanitizedConfig,
|
|
44
|
+
context: req.context || {},
|
|
45
|
+
currentDepth: 1,
|
|
46
|
+
depth,
|
|
47
|
+
doc,
|
|
48
|
+
draft: false,
|
|
49
|
+
fallbackLocale: req.fallbackLocale || 'en',
|
|
50
|
+
findMany: false,
|
|
51
|
+
flattenLocales: true,
|
|
52
|
+
global: null,
|
|
53
|
+
locale: req.locale || 'en',
|
|
54
|
+
overrideAccess: false,
|
|
55
|
+
populate: populateOption,
|
|
56
|
+
req,
|
|
57
|
+
select: undefined,
|
|
58
|
+
showHiddenFields: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return doc;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
14
64
|
if (db?.query) {
|
|
65
|
+
// Dynamic collection storage is quarantined in sqlAdapter.ts until this
|
|
66
|
+
// layer is redesigned around typed tables that Drizzle can model directly.
|
|
15
67
|
const tableName = config.slug;
|
|
16
68
|
// Ensure id is a string for consistent comparison
|
|
17
69
|
const idString = String(id);
|
|
18
|
-
const query =
|
|
70
|
+
const query = selectByIdQuery(tableName);
|
|
19
71
|
const result = await db.query(query, [idString]);
|
|
20
72
|
const rawDoc = result.rows[0];
|
|
21
73
|
if (!rawDoc) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DatabaseResult } from '../../types/index.js';
|
|
2
|
+
export type QueryableCollectionDb = {
|
|
3
|
+
query: (query: string, values?: unknown[]) => Promise<DatabaseResult>;
|
|
4
|
+
};
|
|
5
|
+
export declare function validateSlug(slug: string): void;
|
|
6
|
+
export declare function validateColumnName(column: string): void;
|
|
7
|
+
export declare function escapeIdentifier(identifier: string): string;
|
|
8
|
+
export declare function collectionTable(configSlug: string): string;
|
|
9
|
+
export declare function selectByIdQuery(configSlug: string): string;
|
|
10
|
+
export declare function deleteByIdQuery(configSlug: string): string;
|
|
11
|
+
export declare function countDocumentsQuery(configSlug: string, whereClause?: string): string;
|
|
12
|
+
export declare function listDocumentsQuery(configSlug: string, whereClause: string, orderByClause: string, limitParam: number, offsetParam: number): string;
|
|
13
|
+
export declare function insertDocumentQuery(configSlug: string, columns: string[]): string;
|
|
14
|
+
export declare function selectJsonByIdQuery(configSlug: string): string;
|
|
15
|
+
export declare function checkExistsByIdQuery(configSlug: string): string;
|
|
16
|
+
export declare function updateByIdQuery(configSlug: string, keys: string[]): string;
|
|
17
|
+
/**
|
|
18
|
+
* Version-aware UPDATE: includes `version = version + 1` in SET
|
|
19
|
+
* and `AND version = $N` in WHERE for optimistic locking.
|
|
20
|
+
* Returns the number of affected rows (0 = conflict).
|
|
21
|
+
*/
|
|
22
|
+
export declare function updateByIdWithVersionQuery(configSlug: string, keys: string[]): string;
|
|
23
|
+
//# sourceMappingURL=sqlAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlAdapter.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/sqlAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACvE,CAAC;AAgBF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAM/C;AAKD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAMvD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpF;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAKjF;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAI1E;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAKrF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic collections resolve table and column names at runtime from config.
|
|
3
|
+
* Drizzle is the default elsewhere in the codebase. This adapter exists only
|
|
4
|
+
* because these collection operations target runtime-selected tables/columns
|
|
5
|
+
* that do not map cleanly to Drizzle's compile-time table model yet.
|
|
6
|
+
*
|
|
7
|
+
* Treat this as a temporary quarantine boundary for dynamic SQL. Do not add
|
|
8
|
+
* inline SQL back into the collection operations; extend this adapter or
|
|
9
|
+
* redesign the collection storage layer toward typed tables instead.
|
|
10
|
+
*/
|
|
11
|
+
/** Only lowercase alphanumeric, hyphens, and underscores (1-63 chars, PostgreSQL identifier limit). */
|
|
12
|
+
const VALID_SLUG = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
13
|
+
export function validateSlug(slug) {
|
|
14
|
+
if (!VALID_SLUG.test(slug)) {
|
|
15
|
+
throw new Error(`Invalid collection slug: "${slug}". Slugs must start with a lowercase letter and contain only lowercase alphanumeric characters, hyphens, and underscores (max 63 chars).`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Only lowercase alphanumeric and underscores (PostgreSQL column name safe). */
|
|
19
|
+
const VALID_COLUMN = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
20
|
+
export function validateColumnName(column) {
|
|
21
|
+
if (!VALID_COLUMN.test(column)) {
|
|
22
|
+
throw new Error(`Invalid column name: "${column}". Column names must start with a lowercase letter or underscore and contain only lowercase alphanumeric characters and underscores.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function escapeIdentifier(identifier) {
|
|
26
|
+
return identifier.replace(/"/g, '""');
|
|
27
|
+
}
|
|
28
|
+
export function collectionTable(configSlug) {
|
|
29
|
+
validateSlug(configSlug);
|
|
30
|
+
return `"${escapeIdentifier(configSlug)}"`;
|
|
31
|
+
}
|
|
32
|
+
export function selectByIdQuery(configSlug) {
|
|
33
|
+
return `SELECT * FROM ${collectionTable(configSlug)} WHERE id = $1 LIMIT 1`;
|
|
34
|
+
}
|
|
35
|
+
export function deleteByIdQuery(configSlug) {
|
|
36
|
+
return `DELETE FROM ${collectionTable(configSlug)} WHERE id = $1`;
|
|
37
|
+
}
|
|
38
|
+
export function countDocumentsQuery(configSlug, whereClause) {
|
|
39
|
+
return whereClause
|
|
40
|
+
? `SELECT COUNT(*) as total FROM ${collectionTable(configSlug)} WHERE ${whereClause}`
|
|
41
|
+
: `SELECT COUNT(*) as total FROM ${collectionTable(configSlug)}`;
|
|
42
|
+
}
|
|
43
|
+
export function listDocumentsQuery(configSlug, whereClause, orderByClause, limitParam, offsetParam) {
|
|
44
|
+
return `SELECT * FROM ${collectionTable(configSlug)} ${whereClause ? `WHERE ${whereClause}` : ''} ${orderByClause} LIMIT $${limitParam} OFFSET $${offsetParam}`;
|
|
45
|
+
}
|
|
46
|
+
export function insertDocumentQuery(configSlug, columns) {
|
|
47
|
+
for (const col of columns)
|
|
48
|
+
validateColumnName(col);
|
|
49
|
+
const escapedColumns = columns.map((column) => `"${escapeIdentifier(column)}"`).join(', ');
|
|
50
|
+
const placeholders = columns.map((_, i) => `$${i + 2}`).join(', ');
|
|
51
|
+
return `INSERT INTO ${collectionTable(configSlug)} (id, ${escapedColumns}) VALUES ($1, ${placeholders})`;
|
|
52
|
+
}
|
|
53
|
+
export function selectJsonByIdQuery(configSlug) {
|
|
54
|
+
return `SELECT _json FROM ${collectionTable(configSlug)} WHERE id = $1 LIMIT 1`;
|
|
55
|
+
}
|
|
56
|
+
export function checkExistsByIdQuery(configSlug) {
|
|
57
|
+
return `SELECT id FROM ${collectionTable(configSlug)} WHERE id = $1 LIMIT 1`;
|
|
58
|
+
}
|
|
59
|
+
export function updateByIdQuery(configSlug, keys) {
|
|
60
|
+
for (const key of keys)
|
|
61
|
+
validateColumnName(key);
|
|
62
|
+
const setClause = keys.map((key, i) => `"${escapeIdentifier(key)}" = $${i + 1}`).join(', ');
|
|
63
|
+
return `UPDATE ${collectionTable(configSlug)} SET ${setClause} WHERE id = $${keys.length + 1}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Version-aware UPDATE: includes `version = version + 1` in SET
|
|
67
|
+
* and `AND version = $N` in WHERE for optimistic locking.
|
|
68
|
+
* Returns the number of affected rows (0 = conflict).
|
|
69
|
+
*/
|
|
70
|
+
export function updateByIdWithVersionQuery(configSlug, keys) {
|
|
71
|
+
for (const key of keys)
|
|
72
|
+
validateColumnName(key);
|
|
73
|
+
const setClause = keys.map((key, i) => `"${escapeIdentifier(key)}" = $${i + 1}`).join(', ');
|
|
74
|
+
// version param is at keys.length + 1, id param is at keys.length + 2
|
|
75
|
+
return `UPDATE ${collectionTable(configSlug)} SET ${setClause}, "version" = "version" + 1 WHERE id = $${keys.length + 2} AND "version" = $${keys.length + 1}`;
|
|
76
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update Operation
|
|
3
3
|
*
|
|
4
|
-
* Updates an existing document with validation, password hashing, and JSON field handling.
|
|
4
|
+
* Updates an existing document with access control, validation, password hashing, and JSON field handling.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
7
|
-
export declare function update(config: RevealCollectionConfig, db:
|
|
8
|
-
query: (query: string, values?: unknown[]) => Promise<DatabaseResult>;
|
|
9
|
-
} | null, options: RevealUpdateOptions): Promise<RevealDocument>;
|
|
6
|
+
import type { QueryableDatabaseAdapter, RevealCollectionConfig, RevealDocument, RevealUpdateOptions } from '../../types/index.js';
|
|
7
|
+
export declare function update(config: RevealCollectionConfig, db: QueryableDatabaseAdapter | null, options: RevealUpdateOptions): Promise<RevealDocument>;
|
|
10
8
|
//# sourceMappingURL=update.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/update.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/update.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EAGd,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAiF9B,wBAAsB,MAAM,CAC1B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAkMzB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update Operation
|
|
3
3
|
*
|
|
4
|
-
* Updates an existing document with validation, password hashing, and JSON field handling.
|
|
4
|
+
* Updates an existing document with access control, validation, password hashing, and JSON field handling.
|
|
5
5
|
*/
|
|
6
6
|
import bcrypt from 'bcryptjs';
|
|
7
7
|
import { defaultLogger } from '../../instance/logger.js';
|
|
@@ -9,10 +9,61 @@ import { collectJsonFields, serializeValueForDatabase } from '../../utils/json-p
|
|
|
9
9
|
import { flattenFields, isJsonFieldType } from '../../utils/type-guards.js';
|
|
10
10
|
import { runBeforeFieldHooks } from './fieldHooks.js';
|
|
11
11
|
import { findByID } from './findById.js';
|
|
12
|
+
import { checkExistsByIdQuery, selectJsonByIdQuery, updateByIdQuery, updateByIdWithVersionQuery, } from './sqlAdapter.js';
|
|
13
|
+
/**
|
|
14
|
+
* Recursively deep-merge two plain objects. Arrays, nulls, Dates, and
|
|
15
|
+
* primitives in `source` replace the corresponding key in `target`.
|
|
16
|
+
* Only plain-object vs plain-object pairs are merged recursively.
|
|
17
|
+
*/
|
|
18
|
+
function deepMergeJson(target, source) {
|
|
19
|
+
const result = structuredClone(target);
|
|
20
|
+
for (const key of Object.keys(source)) {
|
|
21
|
+
const sourceVal = source[key];
|
|
22
|
+
const targetVal = result[key];
|
|
23
|
+
if (sourceVal !== null &&
|
|
24
|
+
typeof sourceVal === 'object' &&
|
|
25
|
+
!Array.isArray(sourceVal) &&
|
|
26
|
+
targetVal !== null &&
|
|
27
|
+
typeof targetVal === 'object' &&
|
|
28
|
+
!Array.isArray(targetVal)) {
|
|
29
|
+
result[key] = deepMergeJson(targetVal, sourceVal);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result[key] = structuredClone(sourceVal);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Evaluate a collection's access.update function.
|
|
39
|
+
* Returns true (allow) or false (deny).
|
|
40
|
+
*/
|
|
41
|
+
async function evaluateUpdateAccess(config, options) {
|
|
42
|
+
if (options.overrideAccess)
|
|
43
|
+
return true;
|
|
44
|
+
const accessConfig = config.access;
|
|
45
|
+
const updateAccess = accessConfig?.update;
|
|
46
|
+
// No access rule defined = allow all (backward compatible)
|
|
47
|
+
if (!updateAccess)
|
|
48
|
+
return true;
|
|
49
|
+
const req = options.req;
|
|
50
|
+
if (!req)
|
|
51
|
+
return false; // No request context = deny (safe default)
|
|
52
|
+
const result = await updateAccess({ req, id: options.id, data: options.data });
|
|
53
|
+
if (result === false)
|
|
54
|
+
return false;
|
|
55
|
+
// Any truthy result (true, WhereClause) = allowed for single-document operations
|
|
56
|
+
return !!result;
|
|
57
|
+
}
|
|
12
58
|
export async function update(config, db, options) {
|
|
13
59
|
const { id, data } = options;
|
|
60
|
+
// --- Access control enforcement ---
|
|
61
|
+
const allowed = await evaluateUpdateAccess(config, options);
|
|
62
|
+
if (!allowed) {
|
|
63
|
+
throw new Error('Access denied: insufficient permissions to update this document');
|
|
64
|
+
}
|
|
14
65
|
// Run beforeValidate field hooks before validation so they can transform values.
|
|
15
|
-
await runBeforeFieldHooks(config, data, 'update', 'beforeValidate');
|
|
66
|
+
await runBeforeFieldHooks(config, data, 'update', 'beforeValidate', undefined, options.req);
|
|
16
67
|
// Validate email format if email field is being updated
|
|
17
68
|
if (config.fields) {
|
|
18
69
|
for (const field of config.fields) {
|
|
@@ -42,13 +93,15 @@ export async function update(config, db, options) {
|
|
|
42
93
|
}
|
|
43
94
|
}
|
|
44
95
|
// Run beforeChange field hooks after validation but before the DB write.
|
|
45
|
-
await runBeforeFieldHooks(config, data, 'update', 'beforeChange');
|
|
96
|
+
await runBeforeFieldHooks(config, data, 'update', 'beforeChange', undefined, options.req);
|
|
46
97
|
// Hash password if present and not already hashed (doesn't start with $2a$ or $2b$)
|
|
47
98
|
if (data.password && typeof data.password === 'string' && !data.password.startsWith('$2')) {
|
|
48
|
-
const saltRounds =
|
|
99
|
+
const saltRounds = 12;
|
|
49
100
|
data.password = await bcrypt.hash(data.password, saltRounds);
|
|
50
101
|
}
|
|
51
102
|
if (db?.query) {
|
|
103
|
+
// Dynamic collection storage is quarantined in sqlAdapter.ts until this
|
|
104
|
+
// layer is redesigned around typed tables that Drizzle can model directly.
|
|
52
105
|
const tableName = config.slug;
|
|
53
106
|
// Build UPDATE query (PostgreSQL uses $1, $2 style)
|
|
54
107
|
// Serialize complex values (objects, arrays) to JSON strings for SQLite
|
|
@@ -57,7 +110,23 @@ export async function update(config, db, options) {
|
|
|
57
110
|
.filter((field) => isJsonFieldType(field) && field.name)
|
|
58
111
|
.map((field) => field.name)
|
|
59
112
|
.filter((name) => typeof name === 'string'));
|
|
60
|
-
|
|
113
|
+
// Build allowlist from collection fields + system columns to prevent SQL injection via crafted keys
|
|
114
|
+
const allowedColumns = new Set([
|
|
115
|
+
'id',
|
|
116
|
+
'createdAt',
|
|
117
|
+
'updatedAt',
|
|
118
|
+
'created_at',
|
|
119
|
+
'updated_at',
|
|
120
|
+
'_json',
|
|
121
|
+
'password',
|
|
122
|
+
]);
|
|
123
|
+
if (config.fields) {
|
|
124
|
+
for (const field of config.fields) {
|
|
125
|
+
if (field.name)
|
|
126
|
+
allowedColumns.add(field.name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const keys = Object.keys(data).filter((k) => k !== 'id' && !jsonFieldNames.has(k) && allowedColumns.has(k));
|
|
61
130
|
const jsonKeys = Object.keys(data).filter((k) => k !== 'id' && jsonFieldNames.has(k));
|
|
62
131
|
// Collect JSON fields to update using collectJsonFields utility
|
|
63
132
|
const jsonUpdates = collectJsonFields(data, jsonFieldNames);
|
|
@@ -67,7 +136,7 @@ export async function update(config, db, options) {
|
|
|
67
136
|
let existingJson = {};
|
|
68
137
|
if (jsonFieldNames.size > 0) {
|
|
69
138
|
// Fetch _json to preserve existing JSON fields (even when only updating non-JSON fields)
|
|
70
|
-
const rawQuery =
|
|
139
|
+
const rawQuery = selectJsonByIdQuery(tableName);
|
|
71
140
|
const rawResult = await db.query(rawQuery, [String(id)]);
|
|
72
141
|
if (!rawResult.rows[0]) {
|
|
73
142
|
throw new Error(`Document with id ${id} not found`);
|
|
@@ -95,7 +164,7 @@ export async function update(config, db, options) {
|
|
|
95
164
|
}
|
|
96
165
|
else if (keys.length > 0) {
|
|
97
166
|
// No JSON fields in collection - just verify document exists
|
|
98
|
-
const checkQuery =
|
|
167
|
+
const checkQuery = checkExistsByIdQuery(tableName);
|
|
99
168
|
const checkResult = await db.query(checkQuery, [String(id)]);
|
|
100
169
|
if (!checkResult.rows[0]) {
|
|
101
170
|
throw new Error(`Document with id ${id} not found`);
|
|
@@ -104,13 +173,12 @@ export async function update(config, db, options) {
|
|
|
104
173
|
// Merge existing JSON with updates (only if we have JSON fields)
|
|
105
174
|
let mergedJson = {};
|
|
106
175
|
if (jsonFieldNames.size > 0) {
|
|
107
|
-
mergedJson =
|
|
176
|
+
mergedJson = deepMergeJson(existingJson, jsonUpdates);
|
|
108
177
|
// Only include _json in UPDATE if there are actual changes or existing JSON to preserve
|
|
109
178
|
if (jsonKeys.length > 0 || Object.keys(existingJson).length > 0) {
|
|
110
179
|
keys.push('_json');
|
|
111
180
|
}
|
|
112
181
|
}
|
|
113
|
-
const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(', ');
|
|
114
182
|
const values = keys.map((key) => {
|
|
115
183
|
if (key === '_json') {
|
|
116
184
|
// Serialize merged JSON fields object to JSON string
|
|
@@ -121,8 +189,32 @@ export async function update(config, db, options) {
|
|
|
121
189
|
});
|
|
122
190
|
// Ensure id is a string for consistent comparison
|
|
123
191
|
const idString = String(id);
|
|
124
|
-
|
|
125
|
-
|
|
192
|
+
// Optimistic locking: if the caller provides a `version` field, use version-aware update
|
|
193
|
+
// to detect concurrent modifications. The version is stripped from the SET clause and
|
|
194
|
+
// moved to the WHERE clause; the DB auto-increments version on success.
|
|
195
|
+
const clientVersion = typeof data.version === 'number' ? data.version : undefined;
|
|
196
|
+
const updateKeys = clientVersion !== undefined ? keys.filter((k) => k !== 'version') : keys;
|
|
197
|
+
const updateValues = clientVersion !== undefined
|
|
198
|
+
? updateKeys.map((key) => {
|
|
199
|
+
if (key === '_json')
|
|
200
|
+
return serializeValueForDatabase(mergedJson);
|
|
201
|
+
return serializeValueForDatabase(data[key]);
|
|
202
|
+
})
|
|
203
|
+
: values;
|
|
204
|
+
if (clientVersion !== undefined) {
|
|
205
|
+
const query = updateByIdWithVersionQuery(tableName, updateKeys);
|
|
206
|
+
const result = await db.query(query, [...updateValues, clientVersion, idString]);
|
|
207
|
+
if (result.rowCount === 0) {
|
|
208
|
+
// Document exists but version mismatch — concurrent edit detected
|
|
209
|
+
const err = new Error('Document was modified by another user. Refresh and try again.');
|
|
210
|
+
err.statusCode = 409;
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const query = updateByIdQuery(tableName, updateKeys);
|
|
216
|
+
await db.query(query, [...updateValues, idString]);
|
|
217
|
+
}
|
|
126
218
|
// Return updated document (use idString for consistency)
|
|
127
219
|
const updatedDoc = await findByID(config, db, { id: idString });
|
|
128
220
|
if (!updatedDoc) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* updateMany Operation
|
|
3
|
+
*
|
|
4
|
+
* Updates multiple documents in a single transaction. All-or-nothing: if any
|
|
5
|
+
* update fails, the entire batch is rolled back.
|
|
6
|
+
*
|
|
7
|
+
* Documents without a db adapter fall back to in-memory objects (no transaction).
|
|
8
|
+
*/
|
|
9
|
+
import type { BatchResult, BatchUpdateOptions, QueryableDatabaseAdapter, RevealCollectionConfig, RevealDocument } from '../../types/index.js';
|
|
10
|
+
export declare function updateMany(config: RevealCollectionConfig, db: QueryableDatabaseAdapter | null, options: BatchUpdateOptions): Promise<BatchResult<RevealDocument>>;
|
|
11
|
+
//# sourceMappingURL=updateMany.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateMany.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/updateMany.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAClB,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAG9B,wBAAsB,UAAU,CAC9B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CA2CtC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* updateMany Operation
|
|
3
|
+
*
|
|
4
|
+
* Updates multiple documents in a single transaction. All-or-nothing: if any
|
|
5
|
+
* update fails, the entire batch is rolled back.
|
|
6
|
+
*
|
|
7
|
+
* Documents without a db adapter fall back to in-memory objects (no transaction).
|
|
8
|
+
*/
|
|
9
|
+
import { update } from './update.js';
|
|
10
|
+
export async function updateMany(config, db, options) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const errors = [];
|
|
13
|
+
if (!db?.query || options.updates.length === 0) {
|
|
14
|
+
// No DB or empty batch: run each update independently (no transaction available).
|
|
15
|
+
for (const [i, item] of options.updates.entries()) {
|
|
16
|
+
try {
|
|
17
|
+
const doc = await update(config, db, {
|
|
18
|
+
id: item.id,
|
|
19
|
+
data: item.data,
|
|
20
|
+
req: options.req,
|
|
21
|
+
overrideAccess: options.overrideAccess,
|
|
22
|
+
});
|
|
23
|
+
results.push(doc);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
errors.push({ index: i, error: error instanceof Error ? error.message : String(error) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { results, errors };
|
|
30
|
+
}
|
|
31
|
+
// Wrap all updates in a transaction: stop on first error, rollback on failure.
|
|
32
|
+
await db.query('BEGIN');
|
|
33
|
+
try {
|
|
34
|
+
for (const item of options.updates) {
|
|
35
|
+
const doc = await update(config, db, {
|
|
36
|
+
id: item.id,
|
|
37
|
+
data: item.data,
|
|
38
|
+
req: options.req,
|
|
39
|
+
overrideAccess: options.overrideAccess,
|
|
40
|
+
});
|
|
41
|
+
results.push(doc);
|
|
42
|
+
}
|
|
43
|
+
await db.query('COMMIT');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await db.query('ROLLBACK');
|
|
47
|
+
const index = results.length; // Index of the failing item
|
|
48
|
+
errors.push({ index, error: error instanceof Error ? error.message : String(error) });
|
|
49
|
+
return { results: [], errors };
|
|
50
|
+
}
|
|
51
|
+
return { results, errors };
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RevealCollectionConfig } from '../types/index.js';
|
|
2
|
+
export type CollectionStorageMode = 'dynamic' | 'typed-candidate';
|
|
3
|
+
export interface CollectionStorageDescriptor {
|
|
4
|
+
slug: string;
|
|
5
|
+
tableName: string;
|
|
6
|
+
storageMode: CollectionStorageMode;
|
|
7
|
+
allowedColumns: string[];
|
|
8
|
+
jsonFieldNames: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function buildCollectionStorageDescriptor(collection: RevealCollectionConfig): CollectionStorageDescriptor;
|
|
11
|
+
export declare function buildCollectionStorageRegistry(collections: RevealCollectionConfig[] | undefined): Record<string, CollectionStorageDescriptor>;
|
|
12
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/collections/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,mBAAmB,CAAC;AAG/E,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,iBAAiB,CAAC;AAElE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,qBAAqB,CAAC;IACnC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAID,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,sBAAsB,GACjC,2BAA2B,CA6B7B;AAED,wBAAgB,8BAA8B,CAC5C,WAAW,EAAE,sBAAsB,EAAE,GAAG,SAAS,GAChD,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAM7C"}
|