@open-mercato/shared 0.4.6-develop-af28b566dd → 0.4.6-develop-4d77832982
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/bootstrap/dynamicLoader.js +3 -1
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +9 -0
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/crud/api-interceptor.js +1 -0
- package/dist/lib/crud/api-interceptor.js.map +7 -0
- package/dist/lib/crud/factory.js +414 -11
- package/dist/lib/crud/factory.js.map +3 -3
- package/dist/lib/crud/interceptor-registry.js +82 -0
- package/dist/lib/crud/interceptor-registry.js.map +7 -0
- package/dist/lib/crud/interceptor-runner.js +175 -0
- package/dist/lib/crud/interceptor-runner.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/widgets/component-registry.js +79 -0
- package/dist/modules/widgets/component-registry.js.map +7 -0
- package/package.json +1 -1
- package/src/lib/bootstrap/dynamicLoader.ts +2 -0
- package/src/lib/bootstrap/factory.ts +13 -0
- package/src/lib/bootstrap/types.ts +12 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +66 -0
- package/src/lib/crud/api-interceptor.ts +65 -0
- package/src/lib/crud/factory.ts +453 -12
- package/src/lib/crud/interceptor-registry.ts +88 -0
- package/src/lib/crud/interceptor-runner.ts +232 -0
- package/src/modules/widgets/component-registry.ts +119 -0
- package/src/modules/widgets/injection.ts +17 -5
package/src/lib/crud/factory.ts
CHANGED
|
@@ -51,6 +51,12 @@ import { createProfiler, shouldEnableProfiler, type Profiler } from '@open-merca
|
|
|
51
51
|
import { getTranslationOverlayPlugin } from '@open-mercato/shared/lib/localization/overlay-plugin'
|
|
52
52
|
import { applyResponseEnrichers, applyResponseEnricherToRecord } from './enricher-runner'
|
|
53
53
|
import type { EnricherContext } from './response-enricher'
|
|
54
|
+
import type { ApiInterceptorMethod, InterceptorRequest, InterceptorResponse } from './api-interceptor'
|
|
55
|
+
import { runApiInterceptorsAfter, runApiInterceptorsBefore } from './interceptor-runner'
|
|
56
|
+
|
|
57
|
+
type RbacServiceLike = {
|
|
58
|
+
getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>
|
|
59
|
+
}
|
|
54
60
|
|
|
55
61
|
export type CrudHooks<TCreate, TUpdate, TList> = {
|
|
56
62
|
beforeList?: (q: TList, ctx: CrudCtx) => Promise<void> | void
|
|
@@ -452,6 +458,32 @@ function handleError(err: unknown): Response {
|
|
|
452
458
|
return json(body, { status: 500 })
|
|
453
459
|
}
|
|
454
460
|
|
|
461
|
+
function normalizeInterceptorRoutePath(request: Request): string {
|
|
462
|
+
try {
|
|
463
|
+
const pathname = new URL(request.url).pathname
|
|
464
|
+
if (pathname.startsWith('/api/')) return pathname.slice(5)
|
|
465
|
+
if (pathname === '/api') return ''
|
|
466
|
+
return pathname.replace(/^\/+/, '')
|
|
467
|
+
} catch {
|
|
468
|
+
return ''
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function toInterceptorHeaders(headers: Headers): Record<string, string> {
|
|
473
|
+
const output: Record<string, string> = {}
|
|
474
|
+
headers.forEach((value, key) => {
|
|
475
|
+
output[key] = value
|
|
476
|
+
})
|
|
477
|
+
return output
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function cleanInterceptorObject(
|
|
481
|
+
value: Record<string, unknown> | undefined,
|
|
482
|
+
): Record<string, unknown> | undefined {
|
|
483
|
+
if (!value || typeof value !== 'object') return undefined
|
|
484
|
+
return Object.fromEntries(Object.entries(value).filter(([, current]) => current !== undefined))
|
|
485
|
+
}
|
|
486
|
+
|
|
455
487
|
function isUuid(v: any): v is string {
|
|
456
488
|
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)
|
|
457
489
|
}
|
|
@@ -783,7 +815,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
783
815
|
|
|
784
816
|
let userFeatures: string[] | undefined
|
|
785
817
|
try {
|
|
786
|
-
const rbac =
|
|
818
|
+
const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined
|
|
787
819
|
if (rbac?.getGrantedFeatures) {
|
|
788
820
|
userFeatures = await rbac.getGrantedFeatures(ctx.auth.sub, {
|
|
789
821
|
tenantId: ctx.auth.tenantId,
|
|
@@ -804,6 +836,101 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
804
836
|
}
|
|
805
837
|
}
|
|
806
838
|
|
|
839
|
+
async function resolveUserFeatures(ctx: CrudCtx): Promise<string[] | undefined> {
|
|
840
|
+
if (!ctx.auth) return undefined
|
|
841
|
+
try {
|
|
842
|
+
const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined
|
|
843
|
+
if (rbac?.getGrantedFeatures) {
|
|
844
|
+
return await rbac.getGrantedFeatures(ctx.auth.sub, {
|
|
845
|
+
tenantId: ctx.auth.tenantId,
|
|
846
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId,
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
// rbacService is optional in some contexts
|
|
851
|
+
}
|
|
852
|
+
return undefined
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const interceptorContextCache = new WeakMap<object, ReturnType<typeof buildInterceptorContextInner>>()
|
|
856
|
+
|
|
857
|
+
async function buildInterceptorContextInner(ctx: CrudCtx) {
|
|
858
|
+
if (!ctx.auth) return null
|
|
859
|
+
return {
|
|
860
|
+
userId: ctx.auth.sub,
|
|
861
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? '',
|
|
862
|
+
tenantId: ctx.auth.tenantId ?? '',
|
|
863
|
+
em: ctx.container.resolve('em'),
|
|
864
|
+
container: ctx.container,
|
|
865
|
+
userFeatures: await resolveUserFeatures(ctx),
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function buildInterceptorContext(ctx: CrudCtx) {
|
|
870
|
+
const cached = interceptorContextCache.get(ctx)
|
|
871
|
+
if (cached) return cached
|
|
872
|
+
const promise = buildInterceptorContextInner(ctx)
|
|
873
|
+
interceptorContextCache.set(ctx, promise)
|
|
874
|
+
return promise
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function applyInterceptorsBefore(args: {
|
|
878
|
+
ctx: CrudCtx
|
|
879
|
+
request: Request
|
|
880
|
+
method: ApiInterceptorMethod
|
|
881
|
+
body?: Record<string, unknown>
|
|
882
|
+
query?: Record<string, unknown>
|
|
883
|
+
}): Promise<{ errorResponse: Response | null; requestPayload: InterceptorRequest; metadataByInterceptor: Record<string, Record<string, unknown> | undefined> }> {
|
|
884
|
+
const interceptorContext = await buildInterceptorContext(args.ctx)
|
|
885
|
+
const requestPayload: InterceptorRequest = {
|
|
886
|
+
method: args.method,
|
|
887
|
+
url: args.request.url,
|
|
888
|
+
body: cleanInterceptorObject(args.body),
|
|
889
|
+
query: cleanInterceptorObject(args.query),
|
|
890
|
+
headers: toInterceptorHeaders(args.request.headers),
|
|
891
|
+
}
|
|
892
|
+
if (!interceptorContext) {
|
|
893
|
+
return { errorResponse: null, requestPayload, metadataByInterceptor: {} }
|
|
894
|
+
}
|
|
895
|
+
const result = await runApiInterceptorsBefore({
|
|
896
|
+
routePath: normalizeInterceptorRoutePath(args.request),
|
|
897
|
+
method: args.method,
|
|
898
|
+
request: requestPayload,
|
|
899
|
+
context: interceptorContext,
|
|
900
|
+
})
|
|
901
|
+
if (!result.ok) {
|
|
902
|
+
return { errorResponse: json(result.body, { status: result.statusCode }), requestPayload, metadataByInterceptor: {} }
|
|
903
|
+
}
|
|
904
|
+
return { errorResponse: null, requestPayload: result.request, metadataByInterceptor: result.metadataByInterceptor }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function applyInterceptorsAfter(args: {
|
|
908
|
+
ctx: CrudCtx
|
|
909
|
+
request: Request
|
|
910
|
+
method: ApiInterceptorMethod
|
|
911
|
+
requestPayload: InterceptorRequest
|
|
912
|
+
metadataByInterceptor: Record<string, Record<string, unknown> | undefined>
|
|
913
|
+
statusCode: number
|
|
914
|
+
body: Record<string, unknown>
|
|
915
|
+
headers?: Record<string, string>
|
|
916
|
+
}): Promise<{ ok: boolean; statusCode: number; body: Record<string, unknown>; headers: Record<string, string> } | null> {
|
|
917
|
+
const interceptorContext = await buildInterceptorContext(args.ctx)
|
|
918
|
+
if (!interceptorContext) return { ok: true, statusCode: args.statusCode, body: args.body, headers: args.headers ?? {} }
|
|
919
|
+
const result = await runApiInterceptorsAfter({
|
|
920
|
+
routePath: normalizeInterceptorRoutePath(args.request),
|
|
921
|
+
method: args.method,
|
|
922
|
+
request: args.requestPayload,
|
|
923
|
+
response: {
|
|
924
|
+
statusCode: args.statusCode,
|
|
925
|
+
body: args.body,
|
|
926
|
+
headers: args.headers ?? {},
|
|
927
|
+
} satisfies InterceptorResponse,
|
|
928
|
+
context: interceptorContext,
|
|
929
|
+
metadataByInterceptor: args.metadataByInterceptor,
|
|
930
|
+
})
|
|
931
|
+
return result
|
|
932
|
+
}
|
|
933
|
+
|
|
807
934
|
/**
|
|
808
935
|
* Apply response enrichers to list payload items.
|
|
809
936
|
* Mutates payload.items and adds payload._meta.
|
|
@@ -927,11 +1054,31 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
927
1054
|
return json({ error: 'Not implemented' }, { status: 501 })
|
|
928
1055
|
}
|
|
929
1056
|
const url = new URL(request.url)
|
|
930
|
-
const
|
|
1057
|
+
const rawQueryParams = Object.fromEntries(url.searchParams.entries())
|
|
931
1058
|
profiler.mark('query_parsed')
|
|
932
|
-
|
|
1059
|
+
let validated = opts.list.schema.parse(rawQueryParams)
|
|
933
1060
|
profiler.mark('query_validated')
|
|
934
1061
|
|
|
1062
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
1063
|
+
ctx,
|
|
1064
|
+
request,
|
|
1065
|
+
method: 'GET',
|
|
1066
|
+
query: (validated as Record<string, unknown>),
|
|
1067
|
+
})
|
|
1068
|
+
if (beforeInterceptors.errorResponse) {
|
|
1069
|
+
finishProfile({ result: 'interceptor_before_blocked' })
|
|
1070
|
+
return beforeInterceptors.errorResponse
|
|
1071
|
+
}
|
|
1072
|
+
const interceptorRequest = beforeInterceptors.requestPayload
|
|
1073
|
+
const interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1074
|
+
if (interceptorRequest.query) {
|
|
1075
|
+
validated = opts.list.schema.parse(interceptorRequest.query)
|
|
1076
|
+
}
|
|
1077
|
+
const queryParams = {
|
|
1078
|
+
...rawQueryParams,
|
|
1079
|
+
...(interceptorRequest.query ?? {}),
|
|
1080
|
+
} as Record<string, unknown>
|
|
1081
|
+
|
|
935
1082
|
await opts.hooks?.beforeList?.(validated as any, ctx)
|
|
936
1083
|
profiler.mark('before_list_hook')
|
|
937
1084
|
|
|
@@ -971,6 +1118,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
971
1118
|
const maybeStoreCrudCache = async (payload: any) => {
|
|
972
1119
|
if (!cacheEnabled || !cache || !cacheKey) return
|
|
973
1120
|
if (!payload || typeof payload !== 'object') return
|
|
1121
|
+
if (Array.isArray(payload)) return
|
|
974
1122
|
const items = Array.isArray((payload as any).items) ? (payload as any).items : []
|
|
975
1123
|
const tags = new Set<string>()
|
|
976
1124
|
const scopeOrgIds = collectScopeOrganizationIds(ctx)
|
|
@@ -1040,6 +1188,24 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1040
1188
|
if (cachedValue) {
|
|
1041
1189
|
cacheStatus = 'hit'
|
|
1042
1190
|
profiler.mark('cache_hit', { generatedAt: cachedValue.generatedAt ?? null })
|
|
1191
|
+
const payload = safeClone(cachedValue.payload)
|
|
1192
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !Array.isArray((payload as any).items)) {
|
|
1193
|
+
cacheStatus = 'miss'
|
|
1194
|
+
profiler.mark('cache_payload_invalid', {
|
|
1195
|
+
payloadType: Array.isArray(payload) ? 'array' : typeof payload,
|
|
1196
|
+
})
|
|
1197
|
+
try {
|
|
1198
|
+
if (cache && cacheKey && typeof cache.delete === 'function') {
|
|
1199
|
+
await cache.delete(cacheKey)
|
|
1200
|
+
}
|
|
1201
|
+
} catch {
|
|
1202
|
+
// ignore cache eviction failure
|
|
1203
|
+
}
|
|
1204
|
+
cachedValue = null
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (cachedValue) {
|
|
1043
1209
|
const payload = safeClone(cachedValue.payload)
|
|
1044
1210
|
const items = Array.isArray((payload as any)?.items) ? (payload as any).items : []
|
|
1045
1211
|
profiler.mark('cache_payload_ready', { itemCount: items.length })
|
|
@@ -1055,6 +1221,24 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1055
1221
|
query: validated,
|
|
1056
1222
|
})
|
|
1057
1223
|
await opts.hooks?.afterList?.(payload, { ...ctx, query: validated as any })
|
|
1224
|
+
const cacheAfterInterceptors = await applyInterceptorsAfter({
|
|
1225
|
+
ctx,
|
|
1226
|
+
request,
|
|
1227
|
+
method: 'GET',
|
|
1228
|
+
requestPayload: interceptorRequest,
|
|
1229
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1230
|
+
statusCode: 200,
|
|
1231
|
+
body: payload as Record<string, unknown>,
|
|
1232
|
+
})
|
|
1233
|
+
if (!cacheAfterInterceptors) {
|
|
1234
|
+
finishProfile({ result: 'interceptor_after_empty', cacheStatus })
|
|
1235
|
+
return json({ error: 'Internal interceptor error' }, { status: 500 })
|
|
1236
|
+
}
|
|
1237
|
+
if (!cacheAfterInterceptors.ok) {
|
|
1238
|
+
finishProfile({ result: 'interceptor_after_failed', cacheStatus })
|
|
1239
|
+
return json(cacheAfterInterceptors.body, { status: cacheAfterInterceptors.statusCode, headers: cacheAfterInterceptors.headers })
|
|
1240
|
+
}
|
|
1241
|
+
Object.assign(payload, cacheAfterInterceptors.body)
|
|
1058
1242
|
await enrichListPayload(payload, ctx, profiler)
|
|
1059
1243
|
logCacheOutcome('hit', items.length)
|
|
1060
1244
|
const response = respondWithPayload(payload)
|
|
@@ -1091,6 +1275,24 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1091
1275
|
})
|
|
1092
1276
|
const emptyPayload = { items: [], total: 0, page: page.page, pageSize: page.pageSize, totalPages: 0 }
|
|
1093
1277
|
await opts.hooks?.afterList?.(emptyPayload, { ...ctx, query: validated as any })
|
|
1278
|
+
const emptyAfterInterceptors = await applyInterceptorsAfter({
|
|
1279
|
+
ctx,
|
|
1280
|
+
request,
|
|
1281
|
+
method: 'GET',
|
|
1282
|
+
requestPayload: interceptorRequest,
|
|
1283
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1284
|
+
statusCode: 200,
|
|
1285
|
+
body: emptyPayload as Record<string, unknown>,
|
|
1286
|
+
})
|
|
1287
|
+
if (!emptyAfterInterceptors) {
|
|
1288
|
+
finishProfile({ result: 'interceptor_after_empty', cacheStatus, itemCount: 0, total: 0 })
|
|
1289
|
+
return json({ error: 'Internal interceptor error' }, { status: 500 })
|
|
1290
|
+
}
|
|
1291
|
+
if (!emptyAfterInterceptors.ok) {
|
|
1292
|
+
finishProfile({ result: 'interceptor_after_failed', cacheStatus, itemCount: 0, total: 0 })
|
|
1293
|
+
return json(emptyAfterInterceptors.body, { status: emptyAfterInterceptors.statusCode, headers: emptyAfterInterceptors.headers })
|
|
1294
|
+
}
|
|
1295
|
+
Object.assign(emptyPayload, emptyAfterInterceptors.body)
|
|
1094
1296
|
await maybeStoreCrudCache(emptyPayload)
|
|
1095
1297
|
logCacheOutcome(cacheStatus, emptyPayload.items.length)
|
|
1096
1298
|
const response = respondWithPayload(emptyPayload)
|
|
@@ -1237,6 +1439,24 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1237
1439
|
}
|
|
1238
1440
|
await opts.hooks?.afterList?.(payload, { ...ctx, query: validated as any })
|
|
1239
1441
|
profiler.mark('after_list_hook')
|
|
1442
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
1443
|
+
ctx,
|
|
1444
|
+
request,
|
|
1445
|
+
method: 'GET',
|
|
1446
|
+
requestPayload: interceptorRequest,
|
|
1447
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1448
|
+
statusCode: 200,
|
|
1449
|
+
body: payload as Record<string, unknown>,
|
|
1450
|
+
})
|
|
1451
|
+
if (!afterInterceptors) {
|
|
1452
|
+
finishProfile({ result: 'interceptor_after_empty', cacheStatus })
|
|
1453
|
+
return json({ error: 'Internal interceptor error' }, { status: 500 })
|
|
1454
|
+
}
|
|
1455
|
+
if (!afterInterceptors.ok) {
|
|
1456
|
+
finishProfile({ result: 'interceptor_after_failed', cacheStatus })
|
|
1457
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
1458
|
+
}
|
|
1459
|
+
Object.assign(payload, afterInterceptors.body)
|
|
1240
1460
|
await enrichListPayload(payload, ctx, profiler)
|
|
1241
1461
|
await maybeStoreCrudCache(payload)
|
|
1242
1462
|
profiler.mark('cache_store_attempt', { cacheEnabled })
|
|
@@ -1268,6 +1488,30 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1268
1488
|
})
|
|
1269
1489
|
const emptyPayload = { items: [], total: 0 }
|
|
1270
1490
|
await opts.hooks?.afterList?.(emptyPayload, { ...ctx, query: validated as any })
|
|
1491
|
+
const fallbackEmptyAfterInterceptors = await applyInterceptorsAfter({
|
|
1492
|
+
ctx,
|
|
1493
|
+
request,
|
|
1494
|
+
method: 'GET',
|
|
1495
|
+
requestPayload: interceptorRequest,
|
|
1496
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1497
|
+
statusCode: 200,
|
|
1498
|
+
body: emptyPayload as Record<string, unknown>,
|
|
1499
|
+
})
|
|
1500
|
+
if (!fallbackEmptyAfterInterceptors) {
|
|
1501
|
+
finishProfile({
|
|
1502
|
+
result: 'interceptor_after_empty',
|
|
1503
|
+
cacheStatus,
|
|
1504
|
+
itemCount: 0,
|
|
1505
|
+
total: 0,
|
|
1506
|
+
branch: 'fallback',
|
|
1507
|
+
})
|
|
1508
|
+
return json({ error: 'Internal interceptor error' }, { status: 500 })
|
|
1509
|
+
}
|
|
1510
|
+
if (!fallbackEmptyAfterInterceptors.ok) {
|
|
1511
|
+
finishProfile({ result: 'interceptor_after_failed', cacheStatus, itemCount: 0, total: 0, branch: 'fallback' })
|
|
1512
|
+
return json(fallbackEmptyAfterInterceptors.body, { status: fallbackEmptyAfterInterceptors.statusCode, headers: fallbackEmptyAfterInterceptors.headers })
|
|
1513
|
+
}
|
|
1514
|
+
Object.assign(emptyPayload, fallbackEmptyAfterInterceptors.body)
|
|
1271
1515
|
await maybeStoreCrudCache(emptyPayload)
|
|
1272
1516
|
logCacheOutcome(cacheStatus, emptyPayload.items.length)
|
|
1273
1517
|
const response = respondWithPayload(emptyPayload)
|
|
@@ -1357,6 +1601,28 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1357
1601
|
const payload = { items: list, total: list.length }
|
|
1358
1602
|
await opts.hooks?.afterList?.(payload, { ...ctx, query: validated as any })
|
|
1359
1603
|
profiler.mark('after_list_hook')
|
|
1604
|
+
const fallbackAfterInterceptors = await applyInterceptorsAfter({
|
|
1605
|
+
ctx,
|
|
1606
|
+
request,
|
|
1607
|
+
method: 'GET',
|
|
1608
|
+
requestPayload: interceptorRequest,
|
|
1609
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1610
|
+
statusCode: 200,
|
|
1611
|
+
body: payload as Record<string, unknown>,
|
|
1612
|
+
})
|
|
1613
|
+
if (!fallbackAfterInterceptors) {
|
|
1614
|
+
finishProfile({
|
|
1615
|
+
result: 'interceptor_after_empty',
|
|
1616
|
+
cacheStatus,
|
|
1617
|
+
branch: 'fallback',
|
|
1618
|
+
})
|
|
1619
|
+
return json({ error: 'Internal interceptor error' }, { status: 500 })
|
|
1620
|
+
}
|
|
1621
|
+
if (!fallbackAfterInterceptors.ok) {
|
|
1622
|
+
finishProfile({ result: 'interceptor_after_failed', cacheStatus, branch: 'fallback' })
|
|
1623
|
+
return json(fallbackAfterInterceptors.body, { status: fallbackAfterInterceptors.statusCode, headers: fallbackAfterInterceptors.headers })
|
|
1624
|
+
}
|
|
1625
|
+
Object.assign(payload, fallbackAfterInterceptors.body)
|
|
1360
1626
|
await enrichListPayload(payload, ctx, profiler)
|
|
1361
1627
|
await maybeStoreCrudCache(payload)
|
|
1362
1628
|
profiler.mark('cache_store_attempt', { cacheEnabled })
|
|
@@ -1394,13 +1660,26 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1394
1660
|
return json({ error: 'Forbidden' }, { status: 403 })
|
|
1395
1661
|
}
|
|
1396
1662
|
const body = await request.json().catch(() => ({}))
|
|
1663
|
+
let interceptorRequestPayload: InterceptorRequest | null = null
|
|
1664
|
+
let interceptorMetadata: Record<string, Record<string, unknown> | undefined> = {}
|
|
1397
1665
|
|
|
1398
1666
|
if (useCommand) {
|
|
1399
1667
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
1400
1668
|
const action = opts.actions!.create!
|
|
1401
1669
|
const parsed = action.schema ? action.schema.parse(body) : body
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1670
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
1671
|
+
ctx,
|
|
1672
|
+
request,
|
|
1673
|
+
method: 'POST',
|
|
1674
|
+
body: parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : undefined,
|
|
1675
|
+
})
|
|
1676
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
1677
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
1678
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1679
|
+
const interceptedBody = interceptorRequestPayload.body ?? {}
|
|
1680
|
+
const reparsed = action.schema ? action.schema.parse(interceptedBody) : interceptedBody
|
|
1681
|
+
const input = action.mapInput ? await action.mapInput({ parsed: reparsed, raw: interceptedBody, ctx }) : reparsed
|
|
1682
|
+
const userMetadata = action.metadata ? await action.metadata({ input, parsed: reparsed, raw: interceptedBody, ctx }) : null
|
|
1404
1683
|
const baseMetadata: CommandLogMetadata = {
|
|
1405
1684
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
1406
1685
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
|
|
@@ -1410,7 +1689,22 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1410
1689
|
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata)
|
|
1411
1690
|
const { result, logEntry } = await commandBus.execute(action.commandId, { input, ctx, metadata: metadataToSend })
|
|
1412
1691
|
const payload = action.response ? action.response({ result, logEntry, ctx }) : result
|
|
1413
|
-
|
|
1692
|
+
let resolvedPayload = await Promise.resolve(payload)
|
|
1693
|
+
if (interceptorRequestPayload && resolvedPayload && typeof resolvedPayload === 'object' && !Array.isArray(resolvedPayload)) {
|
|
1694
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
1695
|
+
ctx,
|
|
1696
|
+
request,
|
|
1697
|
+
method: 'POST',
|
|
1698
|
+
requestPayload: interceptorRequestPayload,
|
|
1699
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1700
|
+
statusCode: action.status ?? 201,
|
|
1701
|
+
body: resolvedPayload as Record<string, unknown>,
|
|
1702
|
+
})
|
|
1703
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
1704
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
1705
|
+
}
|
|
1706
|
+
if (afterInterceptors?.ok) resolvedPayload = afterInterceptors.body
|
|
1707
|
+
}
|
|
1414
1708
|
const status = action.status ?? 201
|
|
1415
1709
|
const response = json(resolvedPayload, { status })
|
|
1416
1710
|
attachOperationHeader(response, logEntry)
|
|
@@ -1424,6 +1718,18 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1424
1718
|
if (!createConfig) throw new Error('Create configuration missing')
|
|
1425
1719
|
|
|
1426
1720
|
let input = createConfig.schema.parse(body)
|
|
1721
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
1722
|
+
ctx,
|
|
1723
|
+
request,
|
|
1724
|
+
method: 'POST',
|
|
1725
|
+
body: input && typeof input === 'object' ? (input as Record<string, unknown>) : undefined,
|
|
1726
|
+
})
|
|
1727
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
1728
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
1729
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1730
|
+
if (interceptorRequestPayload.body) {
|
|
1731
|
+
input = createConfig.schema.parse(interceptorRequestPayload.body)
|
|
1732
|
+
}
|
|
1427
1733
|
const modified = await opts.hooks?.beforeCreate?.(input as any, ctx)
|
|
1428
1734
|
if (modified) input = modified
|
|
1429
1735
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
@@ -1472,6 +1778,21 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1472
1778
|
await invalidateCrudCache(ctx.container, resourceKind, identifiers, ctx.auth.tenantId ?? null, 'created', resourceTargets)
|
|
1473
1779
|
|
|
1474
1780
|
let payload = createConfig.response ? createConfig.response(entity) : { id: String((entity as any)[ormCfg.idField!]) }
|
|
1781
|
+
if (interceptorRequestPayload && payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
1782
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
1783
|
+
ctx,
|
|
1784
|
+
request,
|
|
1785
|
+
method: 'POST',
|
|
1786
|
+
requestPayload: interceptorRequestPayload,
|
|
1787
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1788
|
+
statusCode: 201,
|
|
1789
|
+
body: payload as Record<string, unknown>,
|
|
1790
|
+
})
|
|
1791
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
1792
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
1793
|
+
}
|
|
1794
|
+
if (afterInterceptors?.ok) payload = afterInterceptors.body
|
|
1795
|
+
}
|
|
1475
1796
|
payload = await enrichSingleRecord(payload, ctx)
|
|
1476
1797
|
return json(payload, { status: 201 })
|
|
1477
1798
|
} catch (e) {
|
|
@@ -1498,13 +1819,26 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1498
1819
|
}
|
|
1499
1820
|
const body = await request.json().catch(() => ({}))
|
|
1500
1821
|
const scopeOrganizationId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null
|
|
1822
|
+
let interceptorRequestPayload: InterceptorRequest | null = null
|
|
1823
|
+
let interceptorMetadata: Record<string, Record<string, unknown> | undefined> = {}
|
|
1501
1824
|
|
|
1502
1825
|
if (useCommand) {
|
|
1503
1826
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
1504
1827
|
const action = opts.actions!.update!
|
|
1505
1828
|
const parsed = action.schema ? action.schema.parse(body) : body
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1829
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
1830
|
+
ctx,
|
|
1831
|
+
request,
|
|
1832
|
+
method: 'PUT',
|
|
1833
|
+
body: parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : undefined,
|
|
1834
|
+
})
|
|
1835
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
1836
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
1837
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1838
|
+
const interceptedBody = interceptorRequestPayload.body ?? {}
|
|
1839
|
+
const reparsed = action.schema ? action.schema.parse(interceptedBody) : interceptedBody
|
|
1840
|
+
const input = action.mapInput ? await action.mapInput({ parsed: reparsed, raw: interceptedBody, ctx }) : reparsed
|
|
1841
|
+
const userMetadata = action.metadata ? await action.metadata({ input, parsed: reparsed, raw: interceptedBody, ctx }) : null
|
|
1508
1842
|
const candidateId = normalizeIdentifierValue((input as Record<string, unknown> | null | undefined)?.id)
|
|
1509
1843
|
let mutationGuardValidation: CrudMutationGuardValidationResult | null = null
|
|
1510
1844
|
if (ctx.auth.tenantId && candidateId) {
|
|
@@ -1536,7 +1870,22 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1536
1870
|
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata)
|
|
1537
1871
|
const { result, logEntry } = await commandBus.execute(action.commandId, { input, ctx, metadata: metadataToSend })
|
|
1538
1872
|
const payload = action.response ? action.response({ result, logEntry, ctx }) : result
|
|
1539
|
-
|
|
1873
|
+
let resolvedPayload = await Promise.resolve(payload)
|
|
1874
|
+
if (interceptorRequestPayload && resolvedPayload && typeof resolvedPayload === 'object' && !Array.isArray(resolvedPayload)) {
|
|
1875
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
1876
|
+
ctx,
|
|
1877
|
+
request,
|
|
1878
|
+
method: 'PUT',
|
|
1879
|
+
requestPayload: interceptorRequestPayload,
|
|
1880
|
+
metadataByInterceptor: interceptorMetadata,
|
|
1881
|
+
statusCode: action.status ?? 200,
|
|
1882
|
+
body: resolvedPayload as Record<string, unknown>,
|
|
1883
|
+
})
|
|
1884
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
1885
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
1886
|
+
}
|
|
1887
|
+
if (afterInterceptors?.ok) resolvedPayload = afterInterceptors.body
|
|
1888
|
+
}
|
|
1540
1889
|
const status = action.status ?? 200
|
|
1541
1890
|
const response = json(resolvedPayload, { status })
|
|
1542
1891
|
attachOperationHeader(response, logEntry)
|
|
@@ -1568,6 +1917,18 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1568
1917
|
if (!updateConfig) throw new Error('Update configuration missing')
|
|
1569
1918
|
|
|
1570
1919
|
let input = updateConfig.schema.parse(body)
|
|
1920
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
1921
|
+
ctx,
|
|
1922
|
+
request,
|
|
1923
|
+
method: 'PUT',
|
|
1924
|
+
body: input && typeof input === 'object' ? (input as Record<string, unknown>) : undefined,
|
|
1925
|
+
})
|
|
1926
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
1927
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
1928
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1929
|
+
if (interceptorRequestPayload.body) {
|
|
1930
|
+
input = updateConfig.schema.parse(interceptorRequestPayload.body)
|
|
1931
|
+
}
|
|
1571
1932
|
const modified = await opts.hooks?.beforeUpdate?.(input as any, ctx)
|
|
1572
1933
|
if (modified) input = modified
|
|
1573
1934
|
|
|
@@ -1663,6 +2024,23 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1663
2024
|
})
|
|
1664
2025
|
}
|
|
1665
2026
|
const payload = updateConfig.response ? updateConfig.response(entity) : { success: true }
|
|
2027
|
+
if (interceptorRequestPayload && payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
2028
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
2029
|
+
ctx,
|
|
2030
|
+
request,
|
|
2031
|
+
method: 'PUT',
|
|
2032
|
+
requestPayload: interceptorRequestPayload,
|
|
2033
|
+
metadataByInterceptor: interceptorMetadata,
|
|
2034
|
+
statusCode: 200,
|
|
2035
|
+
body: payload as Record<string, unknown>,
|
|
2036
|
+
})
|
|
2037
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
2038
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
2039
|
+
}
|
|
2040
|
+
if (afterInterceptors?.ok) {
|
|
2041
|
+
return json(afterInterceptors.body, { status: 200, headers: afterInterceptors.headers })
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
1666
2044
|
return json(payload)
|
|
1667
2045
|
} catch (e) {
|
|
1668
2046
|
return handleError(e)
|
|
@@ -1687,14 +2065,35 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1687
2065
|
const useCommand = !!opts.actions?.delete
|
|
1688
2066
|
const url = new URL(request.url)
|
|
1689
2067
|
const scopeOrganizationId = ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null
|
|
2068
|
+
let interceptorRequestPayload: InterceptorRequest | null = null
|
|
2069
|
+
let interceptorMetadata: Record<string, Record<string, unknown> | undefined> = {}
|
|
1690
2070
|
|
|
1691
2071
|
if (useCommand) {
|
|
1692
2072
|
const action = opts.actions!.delete!
|
|
1693
2073
|
const body = await request.json().catch(() => ({}))
|
|
1694
2074
|
const raw = { body, query: Object.fromEntries(url.searchParams.entries()) }
|
|
1695
2075
|
const parsed = action.schema ? action.schema.parse(raw) : raw
|
|
1696
|
-
const
|
|
1697
|
-
|
|
2076
|
+
const interceptorInput =
|
|
2077
|
+
parsed && typeof parsed === 'object' && (parsed as Record<string, unknown>).body && typeof (parsed as Record<string, unknown>).body === 'object'
|
|
2078
|
+
? ((parsed as Record<string, unknown>).body as Record<string, unknown>)
|
|
2079
|
+
: body
|
|
2080
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
2081
|
+
ctx,
|
|
2082
|
+
request,
|
|
2083
|
+
method: 'DELETE',
|
|
2084
|
+
body: interceptorInput,
|
|
2085
|
+
})
|
|
2086
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
2087
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
2088
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
2089
|
+
const interceptedBody = interceptorRequestPayload.body ?? {}
|
|
2090
|
+
const reparsedRaw = {
|
|
2091
|
+
body: interceptedBody,
|
|
2092
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
2093
|
+
}
|
|
2094
|
+
const reparsed = action.schema ? action.schema.parse(reparsedRaw) : reparsedRaw
|
|
2095
|
+
const input = action.mapInput ? await action.mapInput({ parsed: reparsed, raw: reparsedRaw, ctx }) : reparsed
|
|
2096
|
+
const userMetadata = action.metadata ? await action.metadata({ input, parsed: reparsed, raw: reparsedRaw, ctx }) : null
|
|
1698
2097
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
1699
2098
|
const candidateId = normalizeIdentifierValue(
|
|
1700
2099
|
(input as Record<string, unknown> | null | undefined)?.id
|
|
@@ -1728,7 +2127,22 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1728
2127
|
const metadataToSend = mergeCommandMetadata(baseMetadata, userMetadata)
|
|
1729
2128
|
const { result, logEntry } = await commandBus.execute(action.commandId, { input, ctx, metadata: metadataToSend })
|
|
1730
2129
|
const payload = action.response ? action.response({ result, logEntry, ctx }) : result
|
|
1731
|
-
|
|
2130
|
+
let resolvedPayload = await Promise.resolve(payload)
|
|
2131
|
+
if (interceptorRequestPayload && resolvedPayload && typeof resolvedPayload === 'object' && !Array.isArray(resolvedPayload)) {
|
|
2132
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
2133
|
+
ctx,
|
|
2134
|
+
request,
|
|
2135
|
+
method: 'DELETE',
|
|
2136
|
+
requestPayload: interceptorRequestPayload,
|
|
2137
|
+
metadataByInterceptor: interceptorMetadata,
|
|
2138
|
+
statusCode: action.status ?? 200,
|
|
2139
|
+
body: resolvedPayload as Record<string, unknown>,
|
|
2140
|
+
})
|
|
2141
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
2142
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
2143
|
+
}
|
|
2144
|
+
if (afterInterceptors?.ok) resolvedPayload = afterInterceptors.body
|
|
2145
|
+
}
|
|
1732
2146
|
const status = action.status ?? 200
|
|
1733
2147
|
const response = json(resolvedPayload, { status })
|
|
1734
2148
|
attachOperationHeader(response, logEntry)
|
|
@@ -1761,6 +2175,16 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1761
2175
|
? url.searchParams.get('id')
|
|
1762
2176
|
: (await request.json().catch(() => ({}))).id
|
|
1763
2177
|
if (!isUuid(id)) return json({ error: 'ID is required' }, { status: 400 })
|
|
2178
|
+
const beforeInterceptors = await applyInterceptorsBefore({
|
|
2179
|
+
ctx,
|
|
2180
|
+
request,
|
|
2181
|
+
method: 'DELETE',
|
|
2182
|
+
body: idFrom === 'query' ? undefined : ({ id } as Record<string, unknown>),
|
|
2183
|
+
query: idFrom === 'query' ? ({ id } as Record<string, unknown>) : undefined,
|
|
2184
|
+
})
|
|
2185
|
+
if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
|
|
2186
|
+
interceptorRequestPayload = beforeInterceptors.requestPayload
|
|
2187
|
+
interceptorMetadata = beforeInterceptors.metadataByInterceptor
|
|
1764
2188
|
|
|
1765
2189
|
const mutationGuardValidation = ctx.auth.tenantId
|
|
1766
2190
|
? await validateCrudMutationGuard(ctx.container, {
|
|
@@ -1833,6 +2257,23 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1833
2257
|
})
|
|
1834
2258
|
}
|
|
1835
2259
|
const payload = opts.del?.response ? opts.del.response(id) : { success: true }
|
|
2260
|
+
if (interceptorRequestPayload && payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
2261
|
+
const afterInterceptors = await applyInterceptorsAfter({
|
|
2262
|
+
ctx,
|
|
2263
|
+
request,
|
|
2264
|
+
method: 'DELETE',
|
|
2265
|
+
requestPayload: interceptorRequestPayload,
|
|
2266
|
+
metadataByInterceptor: interceptorMetadata,
|
|
2267
|
+
statusCode: 200,
|
|
2268
|
+
body: payload as Record<string, unknown>,
|
|
2269
|
+
})
|
|
2270
|
+
if (afterInterceptors && !afterInterceptors.ok) {
|
|
2271
|
+
return json(afterInterceptors.body, { status: afterInterceptors.statusCode, headers: afterInterceptors.headers })
|
|
2272
|
+
}
|
|
2273
|
+
if (afterInterceptors?.ok) {
|
|
2274
|
+
return json(afterInterceptors.body, { status: 200, headers: afterInterceptors.headers })
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
1836
2277
|
return json(payload)
|
|
1837
2278
|
} catch (e) {
|
|
1838
2279
|
return handleError(e)
|