@open-mercato/shared 0.4.2-canary-c02407ff85
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/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { buildScopedWhere } from "@open-mercato/shared/lib/api/crud";
|
|
4
|
+
import { getAuthFromCookies, getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
|
+
import { SortDir } from "@open-mercato/shared/lib/query/types";
|
|
6
|
+
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
7
|
+
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
8
|
+
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
9
|
+
import {
|
|
10
|
+
extractCustomFieldValuesFromPayload,
|
|
11
|
+
extractAllCustomFieldEntries,
|
|
12
|
+
decorateRecordWithCustomFields,
|
|
13
|
+
loadCustomFieldDefinitionIndex
|
|
14
|
+
} from "./custom-fields.js";
|
|
15
|
+
import { serializeExport, normalizeExportFormat, defaultExportFilename, ensureColumns } from "./exporters.js";
|
|
16
|
+
import { CrudHttpError } from "./errors.js";
|
|
17
|
+
import {
|
|
18
|
+
buildCollectionTags,
|
|
19
|
+
buildRecordTag,
|
|
20
|
+
canonicalizeResourceTag,
|
|
21
|
+
debugCrudCache,
|
|
22
|
+
deriveResourceFromCommandId,
|
|
23
|
+
expandResourceAliases,
|
|
24
|
+
invalidateCrudCache,
|
|
25
|
+
isCrudCacheDebugEnabled,
|
|
26
|
+
isCrudCacheEnabled,
|
|
27
|
+
normalizeIdentifierValue,
|
|
28
|
+
normalizeTagSegment,
|
|
29
|
+
resolveCrudCache
|
|
30
|
+
} from "./cache.js";
|
|
31
|
+
import { deriveCrudSegmentTag } from "./cache-stats.js";
|
|
32
|
+
import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
|
|
33
|
+
const DEFAULT_EXPORT_FORMATS = ["csv", "json", "xml", "markdown"];
|
|
34
|
+
const DEFAULT_EXPORT_BATCH_SIZE = 1e3;
|
|
35
|
+
const MIN_EXPORT_BATCH_SIZE = 100;
|
|
36
|
+
const MAX_EXPORT_BATCH_SIZE = 1e4;
|
|
37
|
+
function resolveAvailableExportFormats(list) {
|
|
38
|
+
if (!list) return [];
|
|
39
|
+
if (list.export?.enabled === false) return [];
|
|
40
|
+
const formats = list.export?.formats && list.export.formats.length > 0 ? [...list.export.formats] : [...DEFAULT_EXPORT_FORMATS];
|
|
41
|
+
if (!list.export?.formats && list.allowCsv && !formats.includes("csv")) formats.push("csv");
|
|
42
|
+
return Array.from(new Set(formats));
|
|
43
|
+
}
|
|
44
|
+
function resolveExportBatchSize(list, requestedPageSize) {
|
|
45
|
+
const fallback = Math.max(requestedPageSize, DEFAULT_EXPORT_BATCH_SIZE);
|
|
46
|
+
const raw = list?.export?.batchSize ?? fallback;
|
|
47
|
+
return Math.min(Math.max(raw, MIN_EXPORT_BATCH_SIZE), MAX_EXPORT_BATCH_SIZE);
|
|
48
|
+
}
|
|
49
|
+
function sanitizeFieldName(base, used, fallbackIndex) {
|
|
50
|
+
const trimmed = base.trim();
|
|
51
|
+
const sanitized = trimmed.replace(/[^a-zA-Z0-9_\-]/g, "_") || `field_${fallbackIndex}`;
|
|
52
|
+
const normalized = /^[A-Za-z_]/.test(sanitized) ? sanitized : `f_${sanitized}`;
|
|
53
|
+
let candidate = normalized;
|
|
54
|
+
let counter = 1;
|
|
55
|
+
while (used.has(candidate)) {
|
|
56
|
+
candidate = `${normalized}_${counter++}`;
|
|
57
|
+
}
|
|
58
|
+
used.add(candidate);
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
function buildExportFromColumns(items, columnsConfig) {
|
|
62
|
+
const used = /* @__PURE__ */ new Set();
|
|
63
|
+
const columns = columnsConfig.map((col, idx) => {
|
|
64
|
+
const fieldName = sanitizeFieldName(col.field || `field_${idx}`, used, idx);
|
|
65
|
+
const header = col.header?.trim().length ? col.header.trim() : col.field || `Field ${idx + 1}`;
|
|
66
|
+
const resolver = col.resolve ? col.resolve : ((item) => item != null ? item[col.field] : void 0);
|
|
67
|
+
return { field: fieldName, header, resolve: resolver };
|
|
68
|
+
});
|
|
69
|
+
const rows = items.map((item) => {
|
|
70
|
+
const row = {};
|
|
71
|
+
columns.forEach((column) => {
|
|
72
|
+
try {
|
|
73
|
+
row[column.field] = column.resolve(item);
|
|
74
|
+
} catch {
|
|
75
|
+
row[column.field] = void 0;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return row;
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
columns: columns.map(({ field, header }) => ({ field, header })),
|
|
82
|
+
rows
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildExportFromCsv(items, csv) {
|
|
86
|
+
const used = /* @__PURE__ */ new Set();
|
|
87
|
+
const columns = csv.headers.map((header, idx) => ({
|
|
88
|
+
field: sanitizeFieldName(header || `column_${idx + 1}`, used, idx),
|
|
89
|
+
header: header || `Column ${idx + 1}`
|
|
90
|
+
}));
|
|
91
|
+
const rows = items.map((item) => {
|
|
92
|
+
const values = csv.row(item) || [];
|
|
93
|
+
const row = {};
|
|
94
|
+
columns.forEach((column, idx) => {
|
|
95
|
+
row[column.field] = values[idx];
|
|
96
|
+
});
|
|
97
|
+
return row;
|
|
98
|
+
});
|
|
99
|
+
return { columns, rows };
|
|
100
|
+
}
|
|
101
|
+
function buildDefaultExport(items) {
|
|
102
|
+
const rows = items.map((item) => {
|
|
103
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
104
|
+
return { ...item };
|
|
105
|
+
}
|
|
106
|
+
return { value: item };
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
columns: ensureColumns(rows),
|
|
110
|
+
rows
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function prepareExportData(items, list) {
|
|
114
|
+
if (list.export?.columns && list.export.columns.length > 0) {
|
|
115
|
+
return buildExportFromColumns(items, list.export.columns);
|
|
116
|
+
}
|
|
117
|
+
if (list.csv) {
|
|
118
|
+
return buildExportFromCsv(items, list.csv);
|
|
119
|
+
}
|
|
120
|
+
const prepared = buildDefaultExport(items);
|
|
121
|
+
return {
|
|
122
|
+
columns: ensureColumns(prepared.rows, prepared.columns),
|
|
123
|
+
rows: prepared.rows
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function finalizeExportFilename(list, format, fallbackBase) {
|
|
127
|
+
const extension = format === "markdown" ? "md" : format;
|
|
128
|
+
const fromExport = list.export?.filename;
|
|
129
|
+
const apply = (value) => {
|
|
130
|
+
if (!value) return null;
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (!trimmed) return null;
|
|
133
|
+
const sanitized = trimmed.replace(/[^a-z0-9_\-\.]/gi, "_");
|
|
134
|
+
const lower = sanitized.toLowerCase();
|
|
135
|
+
if (lower.endsWith(`.${extension}`)) return sanitized;
|
|
136
|
+
const withoutExtension = sanitized.includes(".") ? sanitized.replace(/\.[^.]+$/, "") : sanitized;
|
|
137
|
+
const base = withoutExtension.trim().length > 0 ? withoutExtension : sanitized;
|
|
138
|
+
return `${base}.${extension}`;
|
|
139
|
+
};
|
|
140
|
+
if (typeof fromExport === "function") {
|
|
141
|
+
const computed = apply(fromExport(format));
|
|
142
|
+
if (computed) return computed;
|
|
143
|
+
} else {
|
|
144
|
+
const computed = apply(fromExport);
|
|
145
|
+
if (computed) return computed;
|
|
146
|
+
}
|
|
147
|
+
if (format === "csv" && list.csv?.filename) {
|
|
148
|
+
const csvName = apply(list.csv.filename);
|
|
149
|
+
if (csvName) return csvName;
|
|
150
|
+
}
|
|
151
|
+
return defaultExportFilename(fallbackBase, format);
|
|
152
|
+
}
|
|
153
|
+
function normalizeFullRecordForExport(input) {
|
|
154
|
+
if (!input || typeof input !== "object") return input;
|
|
155
|
+
if (Array.isArray(input)) return input.map((item) => normalizeFullRecordForExport(item));
|
|
156
|
+
const record = {};
|
|
157
|
+
for (const [key, value] of Object.entries(input)) {
|
|
158
|
+
if (key.startsWith("cf_") || key.startsWith("cf:")) continue;
|
|
159
|
+
record[key] = value;
|
|
160
|
+
}
|
|
161
|
+
const custom = extractAllCustomFieldEntries(input);
|
|
162
|
+
for (const [rawKey, value] of Object.entries(custom)) {
|
|
163
|
+
const sanitizedKey = rawKey.replace(/^cf_/, "");
|
|
164
|
+
record[sanitizedKey] = value;
|
|
165
|
+
}
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
function deriveResourceFromActions(actions) {
|
|
169
|
+
if (!actions) return null;
|
|
170
|
+
const ids = [actions.create?.commandId, actions.update?.commandId, actions.delete?.commandId];
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
const resolved = deriveResourceFromCommandId(id);
|
|
173
|
+
if (resolved) return resolved;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function resolveResourceAliasesList(opts, ormEntityName) {
|
|
178
|
+
const eventsResource = opts.events?.module && opts.events?.entity ? `${opts.events.module}.${opts.events.entity}` : null;
|
|
179
|
+
const commandResource = deriveResourceFromActions(opts.actions);
|
|
180
|
+
const rawCandidate = eventsResource ?? commandResource ?? ormEntityName ?? "resource";
|
|
181
|
+
const primary = canonicalizeResourceTag(rawCandidate) ?? "resource";
|
|
182
|
+
return { primary, aliases: [] };
|
|
183
|
+
}
|
|
184
|
+
function mergeCommandMetadata(base, override) {
|
|
185
|
+
if (!override) return base;
|
|
186
|
+
const mergedContext = {
|
|
187
|
+
...base.context ?? {},
|
|
188
|
+
...override.context ?? {}
|
|
189
|
+
};
|
|
190
|
+
const merged = {
|
|
191
|
+
...base,
|
|
192
|
+
...override
|
|
193
|
+
};
|
|
194
|
+
if (Object.keys(mergedContext).length > 0) merged.context = mergedContext;
|
|
195
|
+
else if ("context" in merged) delete merged.context;
|
|
196
|
+
return merged;
|
|
197
|
+
}
|
|
198
|
+
function json(data, init) {
|
|
199
|
+
return new Response(JSON.stringify(data), {
|
|
200
|
+
...init || {},
|
|
201
|
+
headers: { "content-type": "application/json", ...init?.headers || {} }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function attachOperationHeader(res, logEntry) {
|
|
205
|
+
if (!res || !(res instanceof Response)) return res;
|
|
206
|
+
if (!logEntry || typeof logEntry !== "object") return res;
|
|
207
|
+
const undoToken = typeof logEntry.undoToken === "string" ? logEntry.undoToken : null;
|
|
208
|
+
const id = typeof logEntry.id === "string" ? logEntry.id : null;
|
|
209
|
+
const commandId = typeof logEntry.commandId === "string" ? logEntry.commandId : null;
|
|
210
|
+
if (!undoToken || !id || !commandId) return res;
|
|
211
|
+
const actionLabel = typeof logEntry.actionLabel === "string" ? logEntry.actionLabel : null;
|
|
212
|
+
const resourceKind = typeof logEntry.resourceKind === "string" ? logEntry.resourceKind : null;
|
|
213
|
+
const resourceId = typeof logEntry.resourceId === "string" ? logEntry.resourceId : null;
|
|
214
|
+
const createdAt = logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : typeof logEntry.createdAt === "string" ? logEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
215
|
+
const headerValue = serializeOperationMetadata({
|
|
216
|
+
id,
|
|
217
|
+
undoToken,
|
|
218
|
+
commandId,
|
|
219
|
+
actionLabel,
|
|
220
|
+
resourceKind,
|
|
221
|
+
resourceId,
|
|
222
|
+
executedAt: createdAt
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
res.headers.set("x-om-operation", headerValue);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
return res;
|
|
229
|
+
}
|
|
230
|
+
function handleError(err) {
|
|
231
|
+
if (err instanceof Response) return err;
|
|
232
|
+
if (err instanceof CrudHttpError) return json(err.body, { status: err.status });
|
|
233
|
+
if (err instanceof z.ZodError) return json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
234
|
+
const message = err instanceof Error ? err.message : void 0;
|
|
235
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
236
|
+
console.error("[crud] unexpected error", { message, stack, err });
|
|
237
|
+
const body = {
|
|
238
|
+
error: "Internal server error",
|
|
239
|
+
message: "Something went wrong. Please try again later."
|
|
240
|
+
};
|
|
241
|
+
return json(body, { status: 500 });
|
|
242
|
+
}
|
|
243
|
+
function isUuid(v) {
|
|
244
|
+
return typeof v === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
|
|
245
|
+
}
|
|
246
|
+
function resolveAccessLogService(container) {
|
|
247
|
+
try {
|
|
248
|
+
const service = container.resolve?.("accessLogService");
|
|
249
|
+
if (service && typeof service.log === "function") return service;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
try {
|
|
252
|
+
console.warn("[crud] accessLogService not available in container", err);
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
function logForbidden(details) {
|
|
259
|
+
try {
|
|
260
|
+
console.warn("[crud] Forbidden request", details);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function collectFieldNames(items) {
|
|
265
|
+
const set = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
if (!item || typeof item !== "object") continue;
|
|
268
|
+
for (const key of Object.keys(item)) {
|
|
269
|
+
if (typeof key === "string" && key.length > 0) set.add(key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return Array.from(set);
|
|
273
|
+
}
|
|
274
|
+
function determineAccessType(query, total, idField) {
|
|
275
|
+
if (query && typeof query === "object" && query !== null && idField in query) {
|
|
276
|
+
const value = query[idField];
|
|
277
|
+
if (value !== void 0 && value !== null && String(value).length > 0) return "read:item";
|
|
278
|
+
}
|
|
279
|
+
return total > 1 ? "read:list" : "read";
|
|
280
|
+
}
|
|
281
|
+
function createCrudProfiler(resource, operation) {
|
|
282
|
+
const enabled = shouldEnableProfiler(resource);
|
|
283
|
+
return createProfiler({
|
|
284
|
+
scope: `crud:${operation}`,
|
|
285
|
+
target: resource,
|
|
286
|
+
label: `${resource}:${operation}`,
|
|
287
|
+
loggerLabel: "[crud:profile]",
|
|
288
|
+
enabled
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function logCrudAccess(options) {
|
|
292
|
+
const { container, auth, request, items, resourceKind } = options;
|
|
293
|
+
if (!auth) return;
|
|
294
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
295
|
+
const service = resolveAccessLogService(container);
|
|
296
|
+
if (!service) return;
|
|
297
|
+
const idField = options.idField || "id";
|
|
298
|
+
const tenantId = options.tenantId ?? auth.tenantId ?? null;
|
|
299
|
+
const organizationId = options.organizationId ?? auth.orgId ?? null;
|
|
300
|
+
const actorUserId = auth.keyId ?? auth.sub ?? null;
|
|
301
|
+
const fields = options.fields && options.fields.length ? options.fields : collectFieldNames(items);
|
|
302
|
+
const accessType = options.accessType ?? determineAccessType(options.query, items.length, idField);
|
|
303
|
+
const context = {
|
|
304
|
+
resultCount: items.length,
|
|
305
|
+
accessType
|
|
306
|
+
};
|
|
307
|
+
if (options.query && typeof options.query === "object" && options.query !== null) {
|
|
308
|
+
context.queryKeys = Object.keys(options.query);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
if (request) {
|
|
312
|
+
const url = new URL(request.url);
|
|
313
|
+
context.path = url.pathname;
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
const uniqueIds = /* @__PURE__ */ new Set();
|
|
318
|
+
const tasks = [];
|
|
319
|
+
for (const item of items) {
|
|
320
|
+
if (!item || typeof item !== "object") continue;
|
|
321
|
+
const rawId = item[idField];
|
|
322
|
+
const resourceId = normalizeIdentifierValue(rawId);
|
|
323
|
+
if (!resourceId || uniqueIds.has(resourceId)) continue;
|
|
324
|
+
uniqueIds.add(resourceId);
|
|
325
|
+
const payload = {
|
|
326
|
+
tenantId,
|
|
327
|
+
organizationId,
|
|
328
|
+
actorUserId,
|
|
329
|
+
resourceKind,
|
|
330
|
+
resourceId,
|
|
331
|
+
accessType
|
|
332
|
+
};
|
|
333
|
+
if (fields.length > 0) payload.fields = fields;
|
|
334
|
+
if (Object.keys(context).length > 0) payload.context = context;
|
|
335
|
+
tasks.push(
|
|
336
|
+
Promise.resolve(service.log(payload)).catch((err) => {
|
|
337
|
+
try {
|
|
338
|
+
console.error("[crud] failed to record access log", { err, payload });
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
return void 0;
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (tasks.length > 0) await Promise.all(tasks);
|
|
346
|
+
}
|
|
347
|
+
function safeClone(value) {
|
|
348
|
+
try {
|
|
349
|
+
const structuredCloneFn = globalThis.structuredClone;
|
|
350
|
+
if (typeof structuredCloneFn === "function") {
|
|
351
|
+
return structuredCloneFn(value);
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
return JSON.parse(JSON.stringify(value));
|
|
357
|
+
} catch {
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function collectScopeOrganizationIds(ctx) {
|
|
362
|
+
if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
|
|
363
|
+
return Array.from(new Set(ctx.organizationIds));
|
|
364
|
+
}
|
|
365
|
+
const fallback = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
|
|
366
|
+
return [fallback];
|
|
367
|
+
}
|
|
368
|
+
function serializeSearchParams(params) {
|
|
369
|
+
if (!params || params.keys().next().done) return "";
|
|
370
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
371
|
+
params.forEach((value, key) => {
|
|
372
|
+
const existing = grouped.get(key) ?? [];
|
|
373
|
+
existing.push(value);
|
|
374
|
+
grouped.set(key, existing);
|
|
375
|
+
});
|
|
376
|
+
const normalized = Array.from(grouped.entries()).map(([key, values]) => [key, values.sort()]);
|
|
377
|
+
normalized.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
378
|
+
return JSON.stringify(normalized);
|
|
379
|
+
}
|
|
380
|
+
function buildCrudCacheKey(resource, request, ctx) {
|
|
381
|
+
const url = new URL(request.url);
|
|
382
|
+
const scopeIds = collectScopeOrganizationIds(ctx);
|
|
383
|
+
const scopeSegment = scopeIds.length ? scopeIds.map((id) => normalizeTagSegment(id)).sort().join(",") : "none";
|
|
384
|
+
return [
|
|
385
|
+
"crud",
|
|
386
|
+
normalizeTagSegment(resource),
|
|
387
|
+
"GET",
|
|
388
|
+
url.pathname,
|
|
389
|
+
`tenant:${normalizeTagSegment(ctx.auth?.tenantId ?? null)}`,
|
|
390
|
+
`selectedOrg:${normalizeTagSegment(ctx.selectedOrganizationId ?? null)}`,
|
|
391
|
+
`scope:${scopeSegment}`,
|
|
392
|
+
`query:${serializeSearchParams(url.searchParams)}`
|
|
393
|
+
].join("|");
|
|
394
|
+
}
|
|
395
|
+
function extractRecordIds(items, idField) {
|
|
396
|
+
if (!Array.isArray(items) || !items.length) return [];
|
|
397
|
+
const ids = /* @__PURE__ */ new Set();
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
if (!item || typeof item !== "object") continue;
|
|
400
|
+
const rawId = item[idField];
|
|
401
|
+
const id = normalizeIdentifierValue(rawId);
|
|
402
|
+
if (id) ids.add(id);
|
|
403
|
+
}
|
|
404
|
+
return Array.from(ids);
|
|
405
|
+
}
|
|
406
|
+
function makeCrudRoute(opts) {
|
|
407
|
+
const metadata = opts.metadata || {};
|
|
408
|
+
const ormCfg = {
|
|
409
|
+
entity: opts.orm.entity,
|
|
410
|
+
idField: opts.orm.idField ?? "id",
|
|
411
|
+
orgField: opts.orm.orgField === null ? null : opts.orm.orgField ?? "organizationId",
|
|
412
|
+
tenantField: opts.orm.tenantField === null ? null : opts.orm.tenantField ?? "tenantId",
|
|
413
|
+
softDeleteField: opts.orm.softDeleteField === null ? null : opts.orm.softDeleteField ?? "deletedAt"
|
|
414
|
+
};
|
|
415
|
+
const entityName = typeof ormCfg.entity?.name === "string" && ormCfg.entity.name.length > 0 ? ormCfg.entity.name : void 0;
|
|
416
|
+
const resourceInfo = resolveResourceAliasesList(opts, entityName);
|
|
417
|
+
const resourceKind = resourceInfo.primary;
|
|
418
|
+
const resourceAliases = resourceInfo.aliases;
|
|
419
|
+
const resourceTargets = expandResourceAliases(resourceKind, resourceAliases);
|
|
420
|
+
const defaultIdentifierResolver = (entity, _action) => {
|
|
421
|
+
const id = normalizeIdentifierValue(entity[ormCfg.idField]);
|
|
422
|
+
const orgId = ormCfg.orgField ? normalizeIdentifierValue(entity[ormCfg.orgField]) : null;
|
|
423
|
+
const tenantId = ormCfg.tenantField ? normalizeIdentifierValue(entity[ormCfg.tenantField]) : null;
|
|
424
|
+
return {
|
|
425
|
+
id: id ?? "",
|
|
426
|
+
organizationId: orgId ?? null,
|
|
427
|
+
tenantId: tenantId ?? null
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
const identifierResolver = opts.resolveIdentifiers ? (entity, action) => {
|
|
431
|
+
const raw = opts.resolveIdentifiers(entity, action);
|
|
432
|
+
const id = normalizeIdentifierValue(raw?.id);
|
|
433
|
+
const organizationId = normalizeIdentifierValue(raw?.organizationId);
|
|
434
|
+
const tenantId = normalizeIdentifierValue(raw?.tenantId);
|
|
435
|
+
return {
|
|
436
|
+
id: id ?? "",
|
|
437
|
+
organizationId: organizationId ?? null,
|
|
438
|
+
tenantId: tenantId ?? null
|
|
439
|
+
};
|
|
440
|
+
} : defaultIdentifierResolver;
|
|
441
|
+
const listCustomFieldDecorator = opts.list?.decorateCustomFields;
|
|
442
|
+
const indexerConfig = opts.indexer;
|
|
443
|
+
const eventsConfig = opts.events;
|
|
444
|
+
const pickNormalizedIdentifier = (...values) => {
|
|
445
|
+
for (const value of values) {
|
|
446
|
+
const normalized = normalizeIdentifierValue(value);
|
|
447
|
+
if (normalized) return normalized;
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
};
|
|
451
|
+
const extractIdentifierFrom = (...payloads) => {
|
|
452
|
+
const candidates = [];
|
|
453
|
+
for (const payload of payloads) {
|
|
454
|
+
if (!payload || typeof payload !== "object") {
|
|
455
|
+
candidates.push(payload);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
candidates.push(
|
|
459
|
+
payload?.id,
|
|
460
|
+
payload?.shipmentId,
|
|
461
|
+
payload?.paymentId,
|
|
462
|
+
payload?.lineId,
|
|
463
|
+
payload?.adjustmentId,
|
|
464
|
+
payload?.itemId,
|
|
465
|
+
payload?.orderAdjustmentId,
|
|
466
|
+
payload?.orderId,
|
|
467
|
+
payload?.quoteId
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return pickNormalizedIdentifier(...candidates);
|
|
471
|
+
};
|
|
472
|
+
const markCommandResultForIndexing = async (id, action, ctx) => {
|
|
473
|
+
if (!id || !indexerConfig && !eventsConfig) return;
|
|
474
|
+
try {
|
|
475
|
+
const em = ctx.container.resolve("em");
|
|
476
|
+
const entity = await em.findOne(ormCfg.entity, { [ormCfg.idField]: id });
|
|
477
|
+
const de = ctx.container.resolve("dataEngine");
|
|
478
|
+
const identifiers = identifierResolver(
|
|
479
|
+
entity ?? { [ormCfg.idField]: id },
|
|
480
|
+
action
|
|
481
|
+
);
|
|
482
|
+
const scopedIdentifiers = {
|
|
483
|
+
...identifiers,
|
|
484
|
+
organizationId: identifiers.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
485
|
+
tenantId: identifiers.tenantId ?? ctx.auth?.tenantId ?? null
|
|
486
|
+
};
|
|
487
|
+
de.markOrmEntityChange({
|
|
488
|
+
action,
|
|
489
|
+
entity: entity ?? { [ormCfg.idField]: id },
|
|
490
|
+
identifiers: scopedIdentifiers,
|
|
491
|
+
events: eventsConfig,
|
|
492
|
+
indexer: indexerConfig
|
|
493
|
+
});
|
|
494
|
+
await de.flushOrmEntityChanges();
|
|
495
|
+
} catch (err) {
|
|
496
|
+
if (process.env.NODE_ENV !== "production") {
|
|
497
|
+
console.warn("[crud] failed to mark command result for indexing", { err, id, action, resourceKind });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const inferFieldValue = (item, keys) => {
|
|
502
|
+
for (const key of keys) {
|
|
503
|
+
const value = item[key];
|
|
504
|
+
if (typeof value === "string") {
|
|
505
|
+
const trimmed = value.trim();
|
|
506
|
+
if (trimmed.length) return trimmed;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
};
|
|
511
|
+
const decorateItemsWithCustomFields = async (items, ctx) => {
|
|
512
|
+
if (!listCustomFieldDecorator || !Array.isArray(items) || items.length === 0) return items;
|
|
513
|
+
const entityIds = Array.isArray(listCustomFieldDecorator.entityIds) ? listCustomFieldDecorator.entityIds : [listCustomFieldDecorator.entityIds];
|
|
514
|
+
if (!entityIds.length) return items;
|
|
515
|
+
const cfProfiler = createCrudProfiler(resourceKind, "custom_fields");
|
|
516
|
+
cfProfiler.mark("prepare");
|
|
517
|
+
let profileClosed = false;
|
|
518
|
+
const endProfile = (extra) => {
|
|
519
|
+
if (!cfProfiler.enabled || profileClosed) return;
|
|
520
|
+
profileClosed = true;
|
|
521
|
+
cfProfiler.end(extra);
|
|
522
|
+
};
|
|
523
|
+
try {
|
|
524
|
+
const em = ctx.container.resolve("em");
|
|
525
|
+
const organizationIds = Array.isArray(ctx.organizationIds) && ctx.organizationIds.length ? ctx.organizationIds : [ctx.selectedOrganizationId ?? null];
|
|
526
|
+
const definitionIndex = await loadCustomFieldDefinitionIndex({
|
|
527
|
+
em,
|
|
528
|
+
entityIds,
|
|
529
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
530
|
+
organizationIds
|
|
531
|
+
});
|
|
532
|
+
cfProfiler.mark("definitions_loaded", { definitionCount: definitionIndex.size });
|
|
533
|
+
const decoratedItems = items.map((raw) => {
|
|
534
|
+
if (!raw || typeof raw !== "object") return raw;
|
|
535
|
+
const item = raw;
|
|
536
|
+
const context = listCustomFieldDecorator.resolveContext ? listCustomFieldDecorator.resolveContext(raw, ctx) ?? {} : {};
|
|
537
|
+
const organizationId = context.organizationId ?? inferFieldValue(item, ["organization_id", "organizationId"]);
|
|
538
|
+
const tenantId = context.tenantId ?? inferFieldValue(item, ["tenant_id", "tenantId"]) ?? ctx.auth?.tenantId ?? null;
|
|
539
|
+
const decorated = decorateRecordWithCustomFields(item, definitionIndex, {
|
|
540
|
+
organizationId: organizationId ?? null,
|
|
541
|
+
tenantId: tenantId ?? null
|
|
542
|
+
});
|
|
543
|
+
const output = {
|
|
544
|
+
...item,
|
|
545
|
+
customValues: decorated.customValues,
|
|
546
|
+
customFields: decorated.customFields
|
|
547
|
+
};
|
|
548
|
+
return output;
|
|
549
|
+
});
|
|
550
|
+
cfProfiler.mark("decorate_complete", { itemCount: decoratedItems.length });
|
|
551
|
+
endProfile({
|
|
552
|
+
entityIds: entityIds.length,
|
|
553
|
+
itemCount: decoratedItems.length
|
|
554
|
+
});
|
|
555
|
+
return decoratedItems;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.warn("[crud] failed to decorate custom fields", err);
|
|
558
|
+
endProfile({
|
|
559
|
+
result: "error",
|
|
560
|
+
entityIds: entityIds.length,
|
|
561
|
+
itemCount: items.length
|
|
562
|
+
});
|
|
563
|
+
return items;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
async function ensureAuth(request) {
|
|
567
|
+
const auth = request ? await getAuthFromRequest(request) : await getAuthFromCookies();
|
|
568
|
+
if (!auth) return null;
|
|
569
|
+
if (auth.tenantId && !isUuid(auth.tenantId)) return null;
|
|
570
|
+
return auth;
|
|
571
|
+
}
|
|
572
|
+
async function withCtx(request) {
|
|
573
|
+
const container = await createRequestContainer();
|
|
574
|
+
const rawAuth = await ensureAuth(request);
|
|
575
|
+
let scope = null;
|
|
576
|
+
let selectedOrganizationId = null;
|
|
577
|
+
let organizationIds = null;
|
|
578
|
+
if (rawAuth) {
|
|
579
|
+
try {
|
|
580
|
+
scope = await resolveOrganizationScopeForRequest({ container, auth: rawAuth, request });
|
|
581
|
+
} catch {
|
|
582
|
+
scope = null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const scopedTenantId = scope?.tenantId ?? rawAuth?.tenantId ?? null;
|
|
586
|
+
const scopedOrgId = scope ? scope.selectedId ?? null : rawAuth?.orgId ?? null;
|
|
587
|
+
selectedOrganizationId = scopedOrgId;
|
|
588
|
+
const scopedAuth = rawAuth ? {
|
|
589
|
+
...rawAuth,
|
|
590
|
+
tenantId: scopedTenantId ?? null,
|
|
591
|
+
orgId: scopedOrgId ?? null
|
|
592
|
+
} : null;
|
|
593
|
+
const fallbackOrgId = scopedOrgId ?? rawAuth?.orgId ?? null;
|
|
594
|
+
const rawScopeIds = scope?.filterIds;
|
|
595
|
+
const scopedIds = Array.isArray(rawScopeIds) ? rawScopeIds.filter((id) => typeof id === "string" && id.length > 0) : null;
|
|
596
|
+
if (!scope) {
|
|
597
|
+
organizationIds = fallbackOrgId ? [fallbackOrgId] : null;
|
|
598
|
+
} else if (scopedIds === null) {
|
|
599
|
+
organizationIds = scope.allowedIds === null ? null : fallbackOrgId ? [fallbackOrgId] : null;
|
|
600
|
+
} else if (scopedIds.length > 0) {
|
|
601
|
+
organizationIds = Array.from(new Set(scopedIds));
|
|
602
|
+
} else if (fallbackOrgId) {
|
|
603
|
+
const allowedIds = Array.isArray(scope?.allowedIds) ? scope.allowedIds : null;
|
|
604
|
+
let canUseFallback = false;
|
|
605
|
+
if (allowedIds === null) {
|
|
606
|
+
canUseFallback = true;
|
|
607
|
+
} else if (allowedIds.includes(fallbackOrgId) || allowedIds.length === 0) {
|
|
608
|
+
canUseFallback = true;
|
|
609
|
+
}
|
|
610
|
+
if (canUseFallback) {
|
|
611
|
+
organizationIds = [fallbackOrgId];
|
|
612
|
+
} else {
|
|
613
|
+
organizationIds = [];
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
organizationIds = [];
|
|
617
|
+
}
|
|
618
|
+
return { container, auth: scopedAuth, organizationScope: scope, selectedOrganizationId, organizationIds, request };
|
|
619
|
+
}
|
|
620
|
+
async function GET(request) {
|
|
621
|
+
const profiler = createCrudProfiler(resourceKind, "list");
|
|
622
|
+
const requestMeta = { method: request.method };
|
|
623
|
+
try {
|
|
624
|
+
const urlObj = new URL(request.url);
|
|
625
|
+
requestMeta.path = urlObj.pathname;
|
|
626
|
+
requestMeta.url = request.url;
|
|
627
|
+
if (urlObj.search) requestMeta.query = urlObj.search;
|
|
628
|
+
} catch {
|
|
629
|
+
requestMeta.url = request.url;
|
|
630
|
+
}
|
|
631
|
+
profiler.mark("request_received", requestMeta);
|
|
632
|
+
let profileClosed = false;
|
|
633
|
+
const finishProfile = (extra) => {
|
|
634
|
+
if (!profiler.enabled || profileClosed) return;
|
|
635
|
+
profileClosed = true;
|
|
636
|
+
const meta = extra ? { ...requestMeta, ...extra } : { ...requestMeta };
|
|
637
|
+
profiler.end(meta);
|
|
638
|
+
};
|
|
639
|
+
try {
|
|
640
|
+
profiler.mark("resolve_context");
|
|
641
|
+
const ctx = await withCtx(request);
|
|
642
|
+
profiler.mark("context_ready");
|
|
643
|
+
if (!ctx.auth) {
|
|
644
|
+
finishProfile({ reason: "unauthorized" });
|
|
645
|
+
return json({ error: "Unauthorized" }, { status: 401 });
|
|
646
|
+
}
|
|
647
|
+
if (!opts.list) {
|
|
648
|
+
finishProfile({ reason: "list_not_configured" });
|
|
649
|
+
return json({ error: "Not implemented" }, { status: 501 });
|
|
650
|
+
}
|
|
651
|
+
const url = new URL(request.url);
|
|
652
|
+
const queryParams = Object.fromEntries(url.searchParams.entries());
|
|
653
|
+
profiler.mark("query_parsed");
|
|
654
|
+
const validated = opts.list.schema.parse(queryParams);
|
|
655
|
+
profiler.mark("query_validated");
|
|
656
|
+
await opts.hooks?.beforeList?.(validated, ctx);
|
|
657
|
+
profiler.mark("before_list_hook");
|
|
658
|
+
const availableFormats = resolveAvailableExportFormats(opts.list);
|
|
659
|
+
const requestedExport = normalizeExportFormat(queryParams.format);
|
|
660
|
+
const exportRequested = requestedExport != null && availableFormats.includes(requestedExport);
|
|
661
|
+
const requestedPage = Number(queryParams.page ?? 1) || 1;
|
|
662
|
+
const requestedPageSize = Math.min(Math.max(Number(queryParams.pageSize ?? 50) || 50, 1), 100);
|
|
663
|
+
const exportPageSize = exportRequested ? resolveExportBatchSize(opts.list, requestedPageSize) : requestedPageSize;
|
|
664
|
+
const exportScopeParam = queryParams.exportScope ?? queryParams.export_scope;
|
|
665
|
+
const exportScope = typeof exportScopeParam === "string" ? exportScopeParam.toLowerCase() : null;
|
|
666
|
+
const exportFullRequested = exportRequested && (exportScope === "full" || parseBooleanToken(queryParams.full) === true);
|
|
667
|
+
profiler.mark("export_configured", { exportRequested, exportFullRequested });
|
|
668
|
+
const cacheEnabled = isCrudCacheEnabled() && !exportRequested;
|
|
669
|
+
const cacheTimerStart = cacheEnabled && isCrudCacheDebugEnabled() ? process.hrtime.bigint() : null;
|
|
670
|
+
const cache = cacheEnabled ? resolveCrudCache(ctx.container) : null;
|
|
671
|
+
const cacheKey = cacheEnabled ? buildCrudCacheKey(resourceKind, request, ctx) : null;
|
|
672
|
+
let cacheStatus = "miss";
|
|
673
|
+
let cachedValue = null;
|
|
674
|
+
if (cacheEnabled && cache && cacheKey) {
|
|
675
|
+
const rawCached = await cache.get(cacheKey);
|
|
676
|
+
if (rawCached !== null && rawCached !== void 0) {
|
|
677
|
+
if (typeof rawCached === "object" && "payload" in rawCached) {
|
|
678
|
+
cachedValue = rawCached;
|
|
679
|
+
} else {
|
|
680
|
+
cachedValue = { payload: rawCached, generatedAt: Date.now() };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
profiler.mark("cache_checked", { cached: cachedValue !== null });
|
|
685
|
+
const tenantForScope = ctx.auth?.tenantId ?? null;
|
|
686
|
+
const maybeStoreCrudCache = async (payload2) => {
|
|
687
|
+
if (!cacheEnabled || !cache || !cacheKey) return;
|
|
688
|
+
if (!payload2 || typeof payload2 !== "object") return;
|
|
689
|
+
const items = Array.isArray(payload2.items) ? payload2.items : [];
|
|
690
|
+
const tags = /* @__PURE__ */ new Set();
|
|
691
|
+
const scopeOrgIds = collectScopeOrganizationIds(ctx);
|
|
692
|
+
const crudSegment = deriveCrudSegmentTag(resourceKind, request);
|
|
693
|
+
for (const target of resourceTargets) {
|
|
694
|
+
for (const tag of buildCollectionTags(target, tenantForScope, scopeOrgIds)) {
|
|
695
|
+
tags.add(tag);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const recordIds = extractRecordIds(items, ormCfg.idField);
|
|
699
|
+
for (const recordId of recordIds) {
|
|
700
|
+
for (const target of resourceTargets) {
|
|
701
|
+
tags.add(buildRecordTag(target, tenantForScope, recordId));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (crudSegment) {
|
|
705
|
+
tags.add(`crud:segment:${crudSegment}`);
|
|
706
|
+
}
|
|
707
|
+
if (!tags.size) return;
|
|
708
|
+
try {
|
|
709
|
+
await cache.set(cacheKey, { payload: safeClone(payload2), generatedAt: Date.now() }, { tags: Array.from(tags) });
|
|
710
|
+
debugCrudCache("store", {
|
|
711
|
+
resource: resourceKind,
|
|
712
|
+
key: cacheKey,
|
|
713
|
+
tags: Array.from(tags),
|
|
714
|
+
itemCount: items.length
|
|
715
|
+
});
|
|
716
|
+
} catch (err) {
|
|
717
|
+
debugCrudCache("store", {
|
|
718
|
+
resource: resourceKind,
|
|
719
|
+
key: cacheKey,
|
|
720
|
+
error: err instanceof Error ? err.message : String(err)
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const logCacheOutcome = (event, itemCount) => {
|
|
725
|
+
if (!cacheTimerStart) return;
|
|
726
|
+
const elapsedMs = Number(process.hrtime.bigint() - cacheTimerStart) / 1e6;
|
|
727
|
+
debugCrudCache(event, {
|
|
728
|
+
resource: resourceKind,
|
|
729
|
+
key: cacheKey,
|
|
730
|
+
durationMs: Math.round(elapsedMs * 1e3) / 1e3,
|
|
731
|
+
itemCount
|
|
732
|
+
});
|
|
733
|
+
};
|
|
734
|
+
const respondWithPayload = (payload2, extraHeaders) => {
|
|
735
|
+
const headers = extraHeaders ? { ...extraHeaders } : {};
|
|
736
|
+
const warning = payload2 && typeof payload2 === "object" && payload2.meta?.partialIndexWarning;
|
|
737
|
+
if (warning) {
|
|
738
|
+
headers["x-om-partial-index"] = JSON.stringify({
|
|
739
|
+
type: "partial_index",
|
|
740
|
+
entity: warning.entity,
|
|
741
|
+
entityLabel: warning.entityLabel ?? warning.entity,
|
|
742
|
+
baseCount: warning.baseCount ?? null,
|
|
743
|
+
indexedCount: warning.indexedCount ?? null,
|
|
744
|
+
scope: warning.scope ?? "scoped"
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
if (cacheEnabled) {
|
|
748
|
+
headers["x-om-cache"] = cacheStatus;
|
|
749
|
+
}
|
|
750
|
+
return json(payload2, Object.keys(headers).length ? { headers } : void 0);
|
|
751
|
+
};
|
|
752
|
+
if (cachedValue) {
|
|
753
|
+
cacheStatus = "hit";
|
|
754
|
+
profiler.mark("cache_hit", { generatedAt: cachedValue.generatedAt ?? null });
|
|
755
|
+
const payload2 = safeClone(cachedValue.payload);
|
|
756
|
+
const items = Array.isArray(payload2?.items) ? payload2.items : [];
|
|
757
|
+
profiler.mark("cache_payload_ready", { itemCount: items.length });
|
|
758
|
+
await logCrudAccess({
|
|
759
|
+
container: ctx.container,
|
|
760
|
+
auth: ctx.auth,
|
|
761
|
+
request,
|
|
762
|
+
items,
|
|
763
|
+
idField: ormCfg.idField,
|
|
764
|
+
resourceKind,
|
|
765
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
766
|
+
tenantId: ctx.auth.tenantId ?? null,
|
|
767
|
+
query: validated
|
|
768
|
+
});
|
|
769
|
+
await opts.hooks?.afterList?.(payload2, { ...ctx, query: validated });
|
|
770
|
+
logCacheOutcome("hit", items.length);
|
|
771
|
+
const response2 = respondWithPayload(payload2);
|
|
772
|
+
finishProfile({ result: "cache_hit", cacheStatus });
|
|
773
|
+
return response2;
|
|
774
|
+
}
|
|
775
|
+
if (opts.list.entityId && opts.list.fields) {
|
|
776
|
+
profiler.mark("query_engine_prepare");
|
|
777
|
+
const qe = ctx.container.resolve("queryEngine");
|
|
778
|
+
profiler.mark("query_engine_resolved");
|
|
779
|
+
const sortFieldRaw = queryParams.sortField || "id";
|
|
780
|
+
const sortDirRaw = (queryParams.sortDir || "asc").toLowerCase() === "desc" ? SortDir.Desc : SortDir.Asc;
|
|
781
|
+
const sortField = opts.list.sortFieldMap && opts.list.sortFieldMap[sortFieldRaw] || sortFieldRaw;
|
|
782
|
+
const sort = [{ field: sortField, dir: sortDirRaw }];
|
|
783
|
+
const page = exportRequested ? { page: 1, pageSize: exportPageSize } : { page: requestedPage, pageSize: requestedPageSize };
|
|
784
|
+
const filters = exportFullRequested ? {} : opts.list.buildFilters ? await opts.list.buildFilters(validated, ctx) : {};
|
|
785
|
+
const withDeleted = parseBooleanToken(queryParams.withDeleted) === true;
|
|
786
|
+
profiler.mark("filters_ready", { withDeleted });
|
|
787
|
+
if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
|
|
788
|
+
profiler.mark("scope_blocked");
|
|
789
|
+
logForbidden({
|
|
790
|
+
resourceKind,
|
|
791
|
+
action: "list",
|
|
792
|
+
reason: "organization_scope_empty",
|
|
793
|
+
userId: ctx.auth?.sub ?? null,
|
|
794
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
795
|
+
organizationIds: ctx.organizationIds
|
|
796
|
+
});
|
|
797
|
+
const emptyPayload = { items: [], total: 0, page: page.page, pageSize: page.pageSize, totalPages: 0 };
|
|
798
|
+
await opts.hooks?.afterList?.(emptyPayload, { ...ctx, query: validated });
|
|
799
|
+
await maybeStoreCrudCache(emptyPayload);
|
|
800
|
+
logCacheOutcome(cacheStatus, emptyPayload.items.length);
|
|
801
|
+
const response3 = respondWithPayload(emptyPayload);
|
|
802
|
+
finishProfile({ result: "empty_scope", cacheStatus, itemCount: 0, total: 0 });
|
|
803
|
+
return response3;
|
|
804
|
+
}
|
|
805
|
+
const queryOpts = {
|
|
806
|
+
fields: opts.list.fields,
|
|
807
|
+
includeCustomFields: true,
|
|
808
|
+
sort,
|
|
809
|
+
page,
|
|
810
|
+
filters,
|
|
811
|
+
withDeleted
|
|
812
|
+
};
|
|
813
|
+
if (opts.list.customFieldSources) {
|
|
814
|
+
queryOpts.customFieldSources = opts.list.customFieldSources;
|
|
815
|
+
}
|
|
816
|
+
if (opts.list.joins) {
|
|
817
|
+
queryOpts.joins = opts.list.joins;
|
|
818
|
+
}
|
|
819
|
+
if (ormCfg.tenantField) queryOpts.tenantId = ctx.auth.tenantId;
|
|
820
|
+
if (ormCfg.orgField) {
|
|
821
|
+
queryOpts.organizationId = ctx.selectedOrganizationId ?? void 0;
|
|
822
|
+
queryOpts.organizationIds = ctx.organizationIds ?? void 0;
|
|
823
|
+
}
|
|
824
|
+
const queryEntity = String(opts.list.entityId);
|
|
825
|
+
profiler.mark("query_options_ready");
|
|
826
|
+
const queryProfiler = profiler.child("query_engine", { entity: queryEntity });
|
|
827
|
+
const res = await qe.query(opts.list.entityId, { ...queryOpts, profiler: queryProfiler });
|
|
828
|
+
const rawItems = res.items || [];
|
|
829
|
+
let transformedItems = rawItems.map((i) => opts.list.transformItem ? opts.list.transformItem(i) : i);
|
|
830
|
+
profiler.mark("transform_complete", { itemCount: transformedItems.length });
|
|
831
|
+
transformedItems = await decorateItemsWithCustomFields(transformedItems, ctx);
|
|
832
|
+
profiler.mark("custom_fields_complete", { itemCount: transformedItems.length });
|
|
833
|
+
await logCrudAccess({
|
|
834
|
+
container: ctx.container,
|
|
835
|
+
auth: ctx.auth,
|
|
836
|
+
request,
|
|
837
|
+
items: transformedItems,
|
|
838
|
+
idField: ormCfg.idField,
|
|
839
|
+
resourceKind,
|
|
840
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
841
|
+
tenantId: ctx.auth.tenantId ?? null,
|
|
842
|
+
query: validated
|
|
843
|
+
});
|
|
844
|
+
profiler.mark("access_logged");
|
|
845
|
+
if (exportRequested && requestedExport) {
|
|
846
|
+
const total = typeof res.total === "number" ? res.total : rawItems.length;
|
|
847
|
+
const initialExportItems = exportFullRequested ? rawItems.map(normalizeFullRecordForExport) : transformedItems;
|
|
848
|
+
let exportItems = [...initialExportItems];
|
|
849
|
+
if (total > exportItems.length) {
|
|
850
|
+
const exportPageSizeNumber = typeof page.pageSize === "number" ? page.pageSize : exportPageSize;
|
|
851
|
+
const queryBase = { ...queryOpts };
|
|
852
|
+
delete queryBase.page;
|
|
853
|
+
let nextPage = 2;
|
|
854
|
+
while (exportItems.length < total) {
|
|
855
|
+
profiler.mark("export_next_page_request", { page: nextPage });
|
|
856
|
+
const nextRes = await qe.query(opts.list.entityId, {
|
|
857
|
+
...queryBase,
|
|
858
|
+
page: { page: nextPage, pageSize: exportPageSizeNumber },
|
|
859
|
+
profiler: profiler.child("query_engine", { entity: queryEntity, page: nextPage, mode: "export" })
|
|
860
|
+
});
|
|
861
|
+
const nextItemsRaw = nextRes.items || [];
|
|
862
|
+
if (!nextItemsRaw.length) break;
|
|
863
|
+
let nextTransformed = nextItemsRaw.map((i) => opts.list.transformItem ? opts.list.transformItem(i) : i);
|
|
864
|
+
nextTransformed = await decorateItemsWithCustomFields(nextTransformed, ctx);
|
|
865
|
+
const nextExportItems = exportFullRequested ? nextItemsRaw.map(normalizeFullRecordForExport) : nextTransformed;
|
|
866
|
+
exportItems.push(...nextExportItems);
|
|
867
|
+
if (nextExportItems.length < exportPageSizeNumber) break;
|
|
868
|
+
nextPage += 1;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const prepared = exportFullRequested ? { columns: ensureColumns(exportItems), rows: exportItems } : prepareExportData(exportItems, opts.list);
|
|
872
|
+
const fallbackBase = `${opts.events?.entity || resourceKind || "list"}${exportFullRequested ? "_full" : ""}`;
|
|
873
|
+
const filename = finalizeExportFilename(opts.list, requestedExport, fallbackBase);
|
|
874
|
+
const serialized = serializeExport(prepared, requestedExport);
|
|
875
|
+
const exportPayload = { items: exportItems, total, page: 1, pageSize: exportItems.length, totalPages: 1, ...res.meta ? { meta: res.meta } : {} };
|
|
876
|
+
await opts.hooks?.afterList?.(exportPayload, { ...ctx, query: validated });
|
|
877
|
+
profiler.mark("after_list_hook");
|
|
878
|
+
const response3 = new Response(serialized.body, {
|
|
879
|
+
headers: {
|
|
880
|
+
"content-type": serialized.contentType,
|
|
881
|
+
"content-disposition": `attachment; filename="${filename}"`
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
if (res.meta?.partialIndexWarning) {
|
|
885
|
+
response3.headers.set(
|
|
886
|
+
"x-om-partial-index",
|
|
887
|
+
JSON.stringify({
|
|
888
|
+
type: "partial_index",
|
|
889
|
+
entity: res.meta.partialIndexWarning.entity,
|
|
890
|
+
entityLabel: res.meta.partialIndexWarning.entityLabel ?? res.meta.partialIndexWarning.entity,
|
|
891
|
+
baseCount: res.meta.partialIndexWarning.baseCount ?? null,
|
|
892
|
+
indexedCount: res.meta.partialIndexWarning.indexedCount ?? null,
|
|
893
|
+
scope: res.meta.partialIndexWarning.scope ?? "scoped"
|
|
894
|
+
})
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
finishProfile({
|
|
898
|
+
result: "export",
|
|
899
|
+
cacheStatus,
|
|
900
|
+
itemCount: exportItems.length,
|
|
901
|
+
total
|
|
902
|
+
});
|
|
903
|
+
return response3;
|
|
904
|
+
}
|
|
905
|
+
const payload2 = {
|
|
906
|
+
items: transformedItems,
|
|
907
|
+
total: res.total,
|
|
908
|
+
page: page.page || requestedPage,
|
|
909
|
+
pageSize: page.pageSize || requestedPageSize,
|
|
910
|
+
totalPages: Math.ceil(res.total / (Number(page.pageSize) || 1)),
|
|
911
|
+
...res.meta ? { meta: res.meta } : {}
|
|
912
|
+
};
|
|
913
|
+
await opts.hooks?.afterList?.(payload2, { ...ctx, query: validated });
|
|
914
|
+
profiler.mark("after_list_hook");
|
|
915
|
+
await maybeStoreCrudCache(payload2);
|
|
916
|
+
profiler.mark("cache_store_attempt", { cacheEnabled });
|
|
917
|
+
logCacheOutcome(cacheStatus, payload2.items.length);
|
|
918
|
+
const response2 = respondWithPayload(payload2);
|
|
919
|
+
finishProfile({
|
|
920
|
+
result: "ok",
|
|
921
|
+
cacheStatus,
|
|
922
|
+
itemCount: payload2.items.length,
|
|
923
|
+
total: payload2.total ?? payload2.items.length
|
|
924
|
+
});
|
|
925
|
+
return response2;
|
|
926
|
+
}
|
|
927
|
+
profiler.mark("orm_fallback_prepare");
|
|
928
|
+
const em = ctx.container.resolve("em");
|
|
929
|
+
const repo = em.getRepository(ormCfg.entity);
|
|
930
|
+
profiler.mark("orm_repo_ready");
|
|
931
|
+
if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
|
|
932
|
+
profiler.mark("fallback_scope_blocked");
|
|
933
|
+
logForbidden({
|
|
934
|
+
resourceKind,
|
|
935
|
+
action: "list",
|
|
936
|
+
reason: "organization_scope_empty",
|
|
937
|
+
userId: ctx.auth?.sub ?? null,
|
|
938
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
939
|
+
organizationIds: ctx.organizationIds
|
|
940
|
+
});
|
|
941
|
+
const emptyPayload = { items: [], total: 0 };
|
|
942
|
+
await opts.hooks?.afterList?.(emptyPayload, { ...ctx, query: validated });
|
|
943
|
+
await maybeStoreCrudCache(emptyPayload);
|
|
944
|
+
logCacheOutcome(cacheStatus, emptyPayload.items.length);
|
|
945
|
+
const response2 = respondWithPayload(emptyPayload);
|
|
946
|
+
finishProfile({
|
|
947
|
+
result: "empty_scope",
|
|
948
|
+
cacheStatus,
|
|
949
|
+
itemCount: 0,
|
|
950
|
+
total: 0,
|
|
951
|
+
branch: "fallback"
|
|
952
|
+
});
|
|
953
|
+
return response2;
|
|
954
|
+
}
|
|
955
|
+
const where = buildScopedWhere(
|
|
956
|
+
{},
|
|
957
|
+
{
|
|
958
|
+
organizationId: ormCfg.orgField ? ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null : void 0,
|
|
959
|
+
organizationIds: ormCfg.orgField ? ctx.organizationIds ?? void 0 : void 0,
|
|
960
|
+
tenantId: ormCfg.tenantField ? ctx.auth.tenantId : void 0,
|
|
961
|
+
orgField: ormCfg.orgField,
|
|
962
|
+
tenantField: ormCfg.tenantField,
|
|
963
|
+
softDeleteField: ormCfg.softDeleteField
|
|
964
|
+
}
|
|
965
|
+
);
|
|
966
|
+
let list = await repo.find(where);
|
|
967
|
+
profiler.mark("orm_query_complete", { itemCount: Array.isArray(list) ? list.length : 0 });
|
|
968
|
+
list = await decorateItemsWithCustomFields(list, ctx);
|
|
969
|
+
profiler.mark("fallback_custom_fields_complete", { itemCount: Array.isArray(list) ? list.length : 0 });
|
|
970
|
+
await logCrudAccess({
|
|
971
|
+
container: ctx.container,
|
|
972
|
+
auth: ctx.auth,
|
|
973
|
+
request,
|
|
974
|
+
items: list,
|
|
975
|
+
idField: ormCfg.idField,
|
|
976
|
+
resourceKind,
|
|
977
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
978
|
+
tenantId: ctx.auth.tenantId ?? null,
|
|
979
|
+
query: validated
|
|
980
|
+
});
|
|
981
|
+
profiler.mark("access_logged");
|
|
982
|
+
if (exportRequested && requestedExport) {
|
|
983
|
+
const exportItems = exportFullRequested ? list.map(normalizeFullRecordForExport) : list;
|
|
984
|
+
const prepared = exportFullRequested ? { columns: ensureColumns(exportItems), rows: exportItems } : prepareExportData(exportItems, opts.list);
|
|
985
|
+
const fallbackBase = `${opts.events?.entity || resourceKind || "list"}${exportFullRequested ? "_full" : ""}`;
|
|
986
|
+
const filename = finalizeExportFilename(opts.list, requestedExport, fallbackBase);
|
|
987
|
+
const serialized = serializeExport(prepared, requestedExport);
|
|
988
|
+
await opts.hooks?.afterList?.({ items: exportItems, total: exportItems.length, page: 1, pageSize: exportItems.length, totalPages: 1 }, { ...ctx, query: validated });
|
|
989
|
+
profiler.mark("after_list_hook");
|
|
990
|
+
const response2 = new Response(serialized.body, {
|
|
991
|
+
headers: {
|
|
992
|
+
"content-type": serialized.contentType,
|
|
993
|
+
"content-disposition": `attachment; filename="${filename}"`
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
finishProfile({
|
|
997
|
+
result: "export",
|
|
998
|
+
cacheStatus,
|
|
999
|
+
itemCount: exportItems.length,
|
|
1000
|
+
total: exportItems.length,
|
|
1001
|
+
branch: "fallback"
|
|
1002
|
+
});
|
|
1003
|
+
return response2;
|
|
1004
|
+
}
|
|
1005
|
+
const payload = { items: list, total: list.length };
|
|
1006
|
+
await opts.hooks?.afterList?.(payload, { ...ctx, query: validated });
|
|
1007
|
+
profiler.mark("after_list_hook");
|
|
1008
|
+
await maybeStoreCrudCache(payload);
|
|
1009
|
+
profiler.mark("cache_store_attempt", { cacheEnabled });
|
|
1010
|
+
logCacheOutcome(cacheStatus, payload.items.length);
|
|
1011
|
+
const response = respondWithPayload(payload);
|
|
1012
|
+
finishProfile({
|
|
1013
|
+
result: "ok",
|
|
1014
|
+
cacheStatus,
|
|
1015
|
+
itemCount: payload.items.length,
|
|
1016
|
+
total: payload.total,
|
|
1017
|
+
branch: "fallback"
|
|
1018
|
+
});
|
|
1019
|
+
return response;
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
finishProfile({ result: "error" });
|
|
1022
|
+
return handleError(e);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async function POST(request) {
|
|
1026
|
+
try {
|
|
1027
|
+
const useCommand = !!opts.actions?.create;
|
|
1028
|
+
if (!opts.create && !useCommand) return json({ error: "Not implemented" }, { status: 501 });
|
|
1029
|
+
const ctx = await withCtx(request);
|
|
1030
|
+
if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
|
|
1031
|
+
if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
|
|
1032
|
+
logForbidden({
|
|
1033
|
+
resourceKind,
|
|
1034
|
+
action: "create",
|
|
1035
|
+
reason: "organization_scope_empty",
|
|
1036
|
+
userId: ctx.auth?.sub ?? null,
|
|
1037
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1038
|
+
organizationIds: ctx.organizationIds
|
|
1039
|
+
});
|
|
1040
|
+
return json({ error: "Forbidden" }, { status: 403 });
|
|
1041
|
+
}
|
|
1042
|
+
const body = await request.json().catch(() => ({}));
|
|
1043
|
+
if (useCommand) {
|
|
1044
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1045
|
+
const action = opts.actions.create;
|
|
1046
|
+
const parsed = action.schema ? action.schema.parse(body) : body;
|
|
1047
|
+
const input2 = action.mapInput ? await action.mapInput({ parsed, raw: body, ctx }) : parsed;
|
|
1048
|
+
const userMetadata = action.metadata ? await action.metadata({ input: input2, parsed, raw: body, ctx }) : null;
|
|
1049
|
+
const baseMetadata = {
|
|
1050
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1051
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
1052
|
+
resourceKind,
|
|
1053
|
+
context: { cacheAliases: resourceTargets }
|
|
1054
|
+
};
|
|
1055
|
+
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata);
|
|
1056
|
+
const { result, logEntry } = await commandBus.execute(action.commandId, { input: input2, ctx, metadata: metadataToSend });
|
|
1057
|
+
const payload2 = action.response ? action.response({ result, logEntry, ctx }) : result;
|
|
1058
|
+
const resolvedPayload = await Promise.resolve(payload2);
|
|
1059
|
+
const status = action.status ?? 201;
|
|
1060
|
+
const response = json(resolvedPayload, { status });
|
|
1061
|
+
attachOperationHeader(response, logEntry);
|
|
1062
|
+
const indexedId = extractIdentifierFrom(resolvedPayload, result, parsed);
|
|
1063
|
+
await markCommandResultForIndexing(indexedId, "created", ctx);
|
|
1064
|
+
return response;
|
|
1065
|
+
}
|
|
1066
|
+
const createConfig = opts.create;
|
|
1067
|
+
if (!createConfig) throw new Error("Create configuration missing");
|
|
1068
|
+
let input = createConfig.schema.parse(body);
|
|
1069
|
+
const modified = await opts.hooks?.beforeCreate?.(input, ctx);
|
|
1070
|
+
if (modified) input = modified;
|
|
1071
|
+
const de = ctx.container.resolve("dataEngine");
|
|
1072
|
+
const entityData = createConfig.mapToEntity(input, ctx);
|
|
1073
|
+
const targetOrgId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null;
|
|
1074
|
+
if (ormCfg.orgField) {
|
|
1075
|
+
if (!targetOrgId) return json({ error: "Organization context is required" }, { status: 400 });
|
|
1076
|
+
entityData[ormCfg.orgField] = targetOrgId;
|
|
1077
|
+
}
|
|
1078
|
+
if (ormCfg.tenantField) {
|
|
1079
|
+
if (!ctx.auth.tenantId) return json({ error: "Tenant context is required" }, { status: 400 });
|
|
1080
|
+
entityData[ormCfg.tenantField] = ctx.auth.tenantId;
|
|
1081
|
+
}
|
|
1082
|
+
const entity = await de.createOrmEntity({ entity: ormCfg.entity, data: entityData });
|
|
1083
|
+
if (createConfig.customFields && createConfig.customFields.enabled) {
|
|
1084
|
+
const cfc = createConfig.customFields;
|
|
1085
|
+
const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
|
|
1086
|
+
if (values && Object.keys(values).length > 0) {
|
|
1087
|
+
const de2 = ctx.container.resolve("dataEngine");
|
|
1088
|
+
await de2.setCustomFields({
|
|
1089
|
+
entityId: cfc.entityId,
|
|
1090
|
+
recordId: String(entity[ormCfg.idField]),
|
|
1091
|
+
organizationId: targetOrgId,
|
|
1092
|
+
tenantId: ctx.auth.tenantId,
|
|
1093
|
+
values
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
await opts.hooks?.afterCreate?.(entity, { ...ctx, input });
|
|
1098
|
+
const identifiers = identifierResolver(entity, "created");
|
|
1099
|
+
de.markOrmEntityChange({
|
|
1100
|
+
action: "created",
|
|
1101
|
+
entity,
|
|
1102
|
+
identifiers,
|
|
1103
|
+
events: opts.events,
|
|
1104
|
+
indexer: opts.indexer
|
|
1105
|
+
});
|
|
1106
|
+
await de.flushOrmEntityChanges();
|
|
1107
|
+
await invalidateCrudCache(ctx.container, resourceKind, identifiers, ctx.auth.tenantId ?? null, "created", resourceTargets);
|
|
1108
|
+
const payload = createConfig.response ? createConfig.response(entity) : { id: String(entity[ormCfg.idField]) };
|
|
1109
|
+
return json(payload, { status: 201 });
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
return handleError(e);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async function PUT(request) {
|
|
1115
|
+
try {
|
|
1116
|
+
const useCommand = !!opts.actions?.update;
|
|
1117
|
+
if (!opts.update && !useCommand) return json({ error: "Not implemented" }, { status: 501 });
|
|
1118
|
+
const ctx = await withCtx(request);
|
|
1119
|
+
if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
|
|
1120
|
+
if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
|
|
1121
|
+
logForbidden({
|
|
1122
|
+
resourceKind,
|
|
1123
|
+
action: "update",
|
|
1124
|
+
reason: "organization_scope_empty",
|
|
1125
|
+
userId: ctx.auth?.sub ?? null,
|
|
1126
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1127
|
+
organizationIds: ctx.organizationIds
|
|
1128
|
+
});
|
|
1129
|
+
return json({ error: "Forbidden" }, { status: 403 });
|
|
1130
|
+
}
|
|
1131
|
+
const body = await request.json().catch(() => ({}));
|
|
1132
|
+
if (useCommand) {
|
|
1133
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1134
|
+
const action = opts.actions.update;
|
|
1135
|
+
const parsed = action.schema ? action.schema.parse(body) : body;
|
|
1136
|
+
const input2 = action.mapInput ? await action.mapInput({ parsed, raw: body, ctx }) : parsed;
|
|
1137
|
+
const userMetadata = action.metadata ? await action.metadata({ input: input2, parsed, raw: body, ctx }) : null;
|
|
1138
|
+
const candidateId = normalizeIdentifierValue(input2?.id);
|
|
1139
|
+
const baseMetadata = {
|
|
1140
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1141
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
1142
|
+
resourceKind,
|
|
1143
|
+
context: { cacheAliases: resourceTargets }
|
|
1144
|
+
};
|
|
1145
|
+
if (candidateId) baseMetadata.resourceId = candidateId;
|
|
1146
|
+
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata);
|
|
1147
|
+
const { result, logEntry } = await commandBus.execute(action.commandId, { input: input2, ctx, metadata: metadataToSend });
|
|
1148
|
+
const payload2 = action.response ? action.response({ result, logEntry, ctx }) : result;
|
|
1149
|
+
const resolvedPayload = await Promise.resolve(payload2);
|
|
1150
|
+
const status = action.status ?? 200;
|
|
1151
|
+
const response = json(resolvedPayload, { status });
|
|
1152
|
+
attachOperationHeader(response, logEntry);
|
|
1153
|
+
const indexedId = extractIdentifierFrom(resolvedPayload, result, parsed);
|
|
1154
|
+
await markCommandResultForIndexing(indexedId, "updated", ctx);
|
|
1155
|
+
return response;
|
|
1156
|
+
}
|
|
1157
|
+
const updateConfig = opts.update;
|
|
1158
|
+
if (!updateConfig) throw new Error("Update configuration missing");
|
|
1159
|
+
let input = updateConfig.schema.parse(body);
|
|
1160
|
+
const modified = await opts.hooks?.beforeUpdate?.(input, ctx);
|
|
1161
|
+
if (modified) input = modified;
|
|
1162
|
+
const id = updateConfig.getId ? updateConfig.getId(input) : input.id;
|
|
1163
|
+
if (!isUuid(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1164
|
+
const targetOrgId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null;
|
|
1165
|
+
if (ormCfg.orgField && !targetOrgId) return json({ error: "Organization context is required" }, { status: 400 });
|
|
1166
|
+
const de = ctx.container.resolve("dataEngine");
|
|
1167
|
+
const where = buildScopedWhere(
|
|
1168
|
+
{ [ormCfg.idField]: id },
|
|
1169
|
+
{
|
|
1170
|
+
organizationId: ormCfg.orgField ? targetOrgId : void 0,
|
|
1171
|
+
organizationIds: ormCfg.orgField ? ctx.organizationIds ?? void 0 : void 0,
|
|
1172
|
+
tenantId: ormCfg.tenantField ? ctx.auth.tenantId : void 0,
|
|
1173
|
+
orgField: ormCfg.orgField,
|
|
1174
|
+
tenantField: ormCfg.tenantField,
|
|
1175
|
+
softDeleteField: ormCfg.softDeleteField
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
const entity = await de.updateOrmEntity({
|
|
1179
|
+
entity: ormCfg.entity,
|
|
1180
|
+
where,
|
|
1181
|
+
apply: (e) => updateConfig.applyToEntity(e, input, ctx)
|
|
1182
|
+
});
|
|
1183
|
+
if (!entity) return json({ error: "Not found" }, { status: 404 });
|
|
1184
|
+
if (updateConfig.customFields && updateConfig.customFields.enabled) {
|
|
1185
|
+
const cfc = updateConfig.customFields;
|
|
1186
|
+
const values = cfc.map ? cfc.map(body) : cfc.pickPrefixed ? extractCustomFieldValuesFromPayload(body) : {};
|
|
1187
|
+
if (values && Object.keys(values).length > 0) {
|
|
1188
|
+
const de2 = ctx.container.resolve("dataEngine");
|
|
1189
|
+
await de2.setCustomFields({
|
|
1190
|
+
entityId: cfc.entityId,
|
|
1191
|
+
recordId: String(entity[ormCfg.idField]),
|
|
1192
|
+
organizationId: targetOrgId,
|
|
1193
|
+
tenantId: ctx.auth.tenantId,
|
|
1194
|
+
values
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
await opts.hooks?.afterUpdate?.(entity, { ...ctx, input });
|
|
1199
|
+
const identifiers = identifierResolver(entity, "updated");
|
|
1200
|
+
de.markOrmEntityChange({
|
|
1201
|
+
action: "updated",
|
|
1202
|
+
entity,
|
|
1203
|
+
identifiers,
|
|
1204
|
+
events: opts.events,
|
|
1205
|
+
indexer: opts.indexer
|
|
1206
|
+
});
|
|
1207
|
+
await de.flushOrmEntityChanges();
|
|
1208
|
+
await invalidateCrudCache(ctx.container, resourceKind, identifiers, ctx.auth.tenantId ?? null, "updated", resourceTargets);
|
|
1209
|
+
const payload = updateConfig.response ? updateConfig.response(entity) : { success: true };
|
|
1210
|
+
return json(payload);
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
return handleError(e);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function DELETE(request) {
|
|
1216
|
+
try {
|
|
1217
|
+
const ctx = await withCtx(request);
|
|
1218
|
+
if (!ctx.auth) return json({ error: "Unauthorized" }, { status: 401 });
|
|
1219
|
+
if (ormCfg.orgField && ctx.organizationIds && ctx.organizationIds.length === 0) {
|
|
1220
|
+
logForbidden({
|
|
1221
|
+
resourceKind,
|
|
1222
|
+
action: "delete",
|
|
1223
|
+
reason: "organization_scope_empty",
|
|
1224
|
+
userId: ctx.auth?.sub ?? null,
|
|
1225
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1226
|
+
organizationIds: ctx.organizationIds
|
|
1227
|
+
});
|
|
1228
|
+
return json({ error: "Forbidden" }, { status: 403 });
|
|
1229
|
+
}
|
|
1230
|
+
const useCommand = !!opts.actions?.delete;
|
|
1231
|
+
const url = new URL(request.url);
|
|
1232
|
+
if (useCommand) {
|
|
1233
|
+
const action = opts.actions.delete;
|
|
1234
|
+
const body = await request.json().catch(() => ({}));
|
|
1235
|
+
const raw = { body, query: Object.fromEntries(url.searchParams.entries()) };
|
|
1236
|
+
const parsed = action.schema ? action.schema.parse(raw) : raw;
|
|
1237
|
+
const input = action.mapInput ? await action.mapInput({ parsed, raw, ctx }) : parsed;
|
|
1238
|
+
const userMetadata = action.metadata ? await action.metadata({ input, parsed, raw, ctx }) : null;
|
|
1239
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1240
|
+
const candidateId = normalizeIdentifierValue(
|
|
1241
|
+
input?.id ?? raw.query?.id ?? raw.body?.id
|
|
1242
|
+
);
|
|
1243
|
+
const baseMetadata = {
|
|
1244
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
1245
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
1246
|
+
resourceKind,
|
|
1247
|
+
context: { cacheAliases: resourceTargets }
|
|
1248
|
+
};
|
|
1249
|
+
if (candidateId) baseMetadata.resourceId = candidateId;
|
|
1250
|
+
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata);
|
|
1251
|
+
const { result, logEntry } = await commandBus.execute(action.commandId, { input, ctx, metadata: metadataToSend });
|
|
1252
|
+
const payload2 = action.response ? action.response({ result, logEntry, ctx }) : result;
|
|
1253
|
+
const resolvedPayload = await Promise.resolve(payload2);
|
|
1254
|
+
const status = action.status ?? 200;
|
|
1255
|
+
const response = json(resolvedPayload, { status });
|
|
1256
|
+
attachOperationHeader(response, logEntry);
|
|
1257
|
+
const indexedId = extractIdentifierFrom(resolvedPayload, result, parsed?.body, parsed);
|
|
1258
|
+
await markCommandResultForIndexing(indexedId, "deleted", ctx);
|
|
1259
|
+
return response;
|
|
1260
|
+
}
|
|
1261
|
+
const idFrom = opts.del?.idFrom || "query";
|
|
1262
|
+
const id = idFrom === "query" ? url.searchParams.get("id") : (await request.json().catch(() => ({}))).id;
|
|
1263
|
+
if (!isUuid(id)) return json({ error: "ID is required" }, { status: 400 });
|
|
1264
|
+
const targetOrgId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null;
|
|
1265
|
+
if (ormCfg.orgField && !targetOrgId) return json({ error: "Organization context is required" }, { status: 400 });
|
|
1266
|
+
const de = ctx.container.resolve("dataEngine");
|
|
1267
|
+
const where = buildScopedWhere(
|
|
1268
|
+
{ [ormCfg.idField]: id },
|
|
1269
|
+
{
|
|
1270
|
+
organizationId: ormCfg.orgField ? targetOrgId : void 0,
|
|
1271
|
+
organizationIds: ormCfg.orgField ? ctx.organizationIds ?? void 0 : void 0,
|
|
1272
|
+
tenantId: ormCfg.tenantField ? ctx.auth.tenantId : void 0,
|
|
1273
|
+
orgField: ormCfg.orgField,
|
|
1274
|
+
tenantField: ormCfg.tenantField,
|
|
1275
|
+
softDeleteField: ormCfg.softDeleteField
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
await opts.hooks?.beforeDelete?.(id, ctx);
|
|
1279
|
+
const entity = await de.deleteOrmEntity({
|
|
1280
|
+
entity: ormCfg.entity,
|
|
1281
|
+
where,
|
|
1282
|
+
soft: opts.del?.softDelete !== false,
|
|
1283
|
+
softDeleteField: ormCfg.softDeleteField ?? void 0
|
|
1284
|
+
});
|
|
1285
|
+
if (!entity) return json({ error: "Not found" }, { status: 404 });
|
|
1286
|
+
await opts.hooks?.afterDelete?.(id, ctx);
|
|
1287
|
+
if (entity) {
|
|
1288
|
+
const identifiers = identifierResolver(entity, "deleted");
|
|
1289
|
+
de.markOrmEntityChange({
|
|
1290
|
+
action: "deleted",
|
|
1291
|
+
entity,
|
|
1292
|
+
identifiers,
|
|
1293
|
+
events: opts.events,
|
|
1294
|
+
indexer: opts.indexer
|
|
1295
|
+
});
|
|
1296
|
+
await de.flushOrmEntityChanges();
|
|
1297
|
+
await invalidateCrudCache(ctx.container, resourceKind, identifiers, ctx.auth.tenantId ?? null, "deleted", resourceTargets);
|
|
1298
|
+
}
|
|
1299
|
+
const payload = opts.del?.response ? opts.del.response(id) : { success: true };
|
|
1300
|
+
return json(payload);
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
return handleError(e);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return { metadata, GET, POST, PUT, DELETE };
|
|
1306
|
+
}
|
|
1307
|
+
export {
|
|
1308
|
+
logCrudAccess,
|
|
1309
|
+
makeCrudRoute
|
|
1310
|
+
};
|
|
1311
|
+
//# sourceMappingURL=factory.js.map
|