@happyvertical/smrt-core 0.38.2 → 0.38.4
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/AGENTS.md +11 -2
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts +9 -0
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +1 -1
- package/dist/change-feed.d.ts +19 -10
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +43 -13
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts +83 -0
- package/dist/change-signals.d.ts.map +1 -0
- package/dist/change-signals.js +272 -0
- package/dist/change-signals.js.map +1 -0
- package/dist/collection-cache.d.ts +21 -0
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts +15 -27
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +17 -7
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/cli.d.ts +3 -0
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +7 -4
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/conditional-get.d.ts +17 -6
- package/dist/generators/conditional-get.d.ts.map +1 -1
- package/dist/generators/conditional-get.js +19 -13
- package/dist/generators/conditional-get.js.map +1 -1
- package/dist/generators/events-route.d.ts +72 -0
- package/dist/generators/events-route.d.ts.map +1 -0
- package/dist/generators/events-route.js +234 -0
- package/dist/generators/events-route.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +11 -5
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +3 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +18 -9
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +7 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +77 -13
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +7 -3
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1171 -90
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +9 -3
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
- package/dist/mcp-advisor/types.d.ts +4 -0
- package/dist/mcp-advisor/types.d.ts.map +1 -1
- package/dist/migrations/tracker.js +5 -4
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +1 -1
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/object.d.ts +17 -8
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +42 -26
- package/dist/object.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +12 -0
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/shared-state.d.ts +2 -1
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js.map +1 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +7 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +1 -1
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
- package/dist/signals/sanitizer.d.ts.map +1 -1
- package/dist/signals/sanitizer.js.map +1 -1
- package/dist/smrt-knowledge.json +8 -8
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/notifications-stub.d.ts +24 -0
- package/dist/testing/notifications-stub.d.ts.map +1 -0
- package/dist/testing/notifications-stub.js +62 -0
- package/dist/testing/notifications-stub.js.map +1 -0
- package/dist/testing.js +2 -1
- package/dist/tools/tool-generator.js +1 -1
- package/dist/tools/tool-generator.js.map +1 -1
- package/dist/vite-plugin/changes-route.d.ts +14 -0
- package/dist/vite-plugin/changes-route.d.ts.map +1 -1
- package/dist/vite-plugin/changes-route.js +2 -2
- package/dist/vite-plugin/changes-route.js.map +1 -1
- package/dist/vite-plugin/events-route.d.ts +9 -0
- package/dist/vite-plugin/events-route.d.ts.map +1 -0
- package/dist/vite-plugin/events-route.js +155 -0
- package/dist/vite-plugin/events-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +8 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +8 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +239 -38
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +12 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateConditionalGetRouteHelper } from "../generators/conditional-get.js";
|
|
2
2
|
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
3
3
|
import { generateChangesRoute } from "./changes-route.js";
|
|
4
|
+
import { generateEventsRoute } from "./events-route.js";
|
|
4
5
|
import { generateSyncApplyRoute } from "./sync-apply-route.js";
|
|
5
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { join, relative } from "node:path";
|
|
@@ -175,6 +176,59 @@ function getApiPublicAccess(apiConfig) {
|
|
|
175
176
|
if (value === true || value === "read") return value;
|
|
176
177
|
return false;
|
|
177
178
|
}
|
|
179
|
+
function toFieldColumnAlias(fieldName) {
|
|
180
|
+
return fieldName.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
function manifestObjectKey(objectDef) {
|
|
183
|
+
return objectDef.qualifiedName ?? objectDef.className;
|
|
184
|
+
}
|
|
185
|
+
function manifestObjectExtends(objectDef, parent) {
|
|
186
|
+
const extendsQualified = objectDef.extendsQualified;
|
|
187
|
+
const extendsName = objectDef.extends;
|
|
188
|
+
return typeof extendsQualified === "string" && extendsQualified === parent.qualifiedName || typeof extendsName === "string" && (extendsName === parent.className || extendsName === parent.qualifiedName);
|
|
189
|
+
}
|
|
190
|
+
function findManifestParent(objectDef, manifest) {
|
|
191
|
+
return Object.values(manifest.objects).find((candidate) => manifestObjectExtends(objectDef, candidate));
|
|
192
|
+
}
|
|
193
|
+
function collectStiHierarchyObjects(objectDef, manifest) {
|
|
194
|
+
if (!manifest) return [objectDef];
|
|
195
|
+
let cursor = objectDef;
|
|
196
|
+
let stiBase = cursor.decoratorConfig?.tableStrategy === "sti" ? cursor : void 0;
|
|
197
|
+
while (cursor) {
|
|
198
|
+
const parent = findManifestParent(cursor, manifest);
|
|
199
|
+
if (!parent) break;
|
|
200
|
+
if (parent.decoratorConfig?.tableStrategy === "sti") stiBase = parent;
|
|
201
|
+
cursor = parent;
|
|
202
|
+
}
|
|
203
|
+
if (!stiBase) return [objectDef];
|
|
204
|
+
const members = /* @__PURE__ */ new Map();
|
|
205
|
+
const visit = (current) => {
|
|
206
|
+
const key = manifestObjectKey(current);
|
|
207
|
+
if (members.has(key)) return;
|
|
208
|
+
members.set(key, current);
|
|
209
|
+
for (const candidate of Object.values(manifest.objects)) {
|
|
210
|
+
if (candidate === current) continue;
|
|
211
|
+
if (manifestObjectExtends(candidate, current)) visit(candidate);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
visit(stiBase);
|
|
215
|
+
return [...members.values()];
|
|
216
|
+
}
|
|
217
|
+
function collectReadPermissionFields(objectDef, manifest) {
|
|
218
|
+
const result = [];
|
|
219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const member of collectStiHierarchyObjects(objectDef, manifest)) for (const [name, def] of Object.entries(member.fields ?? {})) {
|
|
221
|
+
const permission = typeof def.readPermission === "string" ? def.readPermission : typeof def._meta?.readPermission === "string" ? def._meta.readPermission : void 0;
|
|
222
|
+
if (!permission) continue;
|
|
223
|
+
for (const fieldName of [name, toFieldColumnAlias(name)]) {
|
|
224
|
+
const key = `${fieldName}\0${permission}`;
|
|
225
|
+
if (seen.has(key)) continue;
|
|
226
|
+
seen.add(key);
|
|
227
|
+
result.push([fieldName, permission]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
178
232
|
/**
|
|
179
233
|
* Emit the fail-closed authorization guard injected into generated route files
|
|
180
234
|
* (#1540, 2c). Every generated CRUD/action handler calls `requireRouteAuth`,
|
|
@@ -182,12 +236,18 @@ function getApiPublicAccess(apiConfig) {
|
|
|
182
236
|
* authenticated principal. Mirrors the knowledge-route `isKnowledgeAdmin`
|
|
183
237
|
* precedent. Mutating verbs require auth even when reads are public.
|
|
184
238
|
*/
|
|
185
|
-
function generateAuthGuardHelper(objectDef) {
|
|
239
|
+
function generateAuthGuardHelper(objectDef, manifest) {
|
|
186
240
|
const publicAccess = getApiPublicAccess(objectDef.decoratorConfig?.api);
|
|
241
|
+
const readPermissionFields = collectReadPermissionFields(objectDef, manifest);
|
|
187
242
|
return `
|
|
188
243
|
// Fail-closed authorization (#1540): generated routes require an authenticated
|
|
189
244
|
// principal on \`locals\` unless explicitly marked \`@smrt({ api: { public } })\`.
|
|
190
245
|
const PUBLIC_ACCESS: boolean | 'read' = ${JSON.stringify(publicAccess)};
|
|
246
|
+
const READ_PERMISSION_FIELDS: Array<[string, string]> = ${JSON.stringify(readPermissionFields)};
|
|
247
|
+
|
|
248
|
+
interface PublicJsonOptions {
|
|
249
|
+
permissions?: Iterable<string>;
|
|
250
|
+
}
|
|
191
251
|
|
|
192
252
|
function hasAuthenticatedPrincipal(locals: unknown): boolean {
|
|
193
253
|
if (!locals || typeof locals !== 'object') return false;
|
|
@@ -215,13 +275,74 @@ function requireRouteAuth(locals: unknown, mutating: boolean): void {
|
|
|
215
275
|
}
|
|
216
276
|
}
|
|
217
277
|
|
|
278
|
+
function getPublicJsonOptions(locals: unknown): PublicJsonOptions {
|
|
279
|
+
const l = readJsonRecord(locals);
|
|
280
|
+
const permissions = l.permissions ?? l.permissionSet ?? l.smrtPermissions;
|
|
281
|
+
if (Array.isArray(permissions) || permissions instanceof Set) {
|
|
282
|
+
return { permissions };
|
|
283
|
+
}
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasReadPermission(
|
|
288
|
+
options: PublicJsonOptions,
|
|
289
|
+
permission: string,
|
|
290
|
+
): boolean {
|
|
291
|
+
if (!options.permissions) return false;
|
|
292
|
+
for (const granted of options.permissions) {
|
|
293
|
+
if (granted === permission) return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function applyReadPermissionRedaction(
|
|
299
|
+
value: unknown,
|
|
300
|
+
options: PublicJsonOptions,
|
|
301
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
302
|
+
): unknown {
|
|
303
|
+
if (value === null || typeof value !== 'object') return value;
|
|
304
|
+
if (seen.has(value)) return null;
|
|
305
|
+
if (hasPublicJson(value)) {
|
|
306
|
+
seen.add(value);
|
|
307
|
+
return applyReadPermissionRedaction(
|
|
308
|
+
value.toPublicJSON(options),
|
|
309
|
+
options,
|
|
310
|
+
seen,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
seen.add(value);
|
|
315
|
+
return value.map((entry) =>
|
|
316
|
+
applyReadPermissionRedaction(entry, options, seen),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const proto = Object.getPrototypeOf(value);
|
|
320
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
321
|
+
seen.add(value);
|
|
322
|
+
const out = { ...(value as Record<string, unknown>) };
|
|
323
|
+
const metaData =
|
|
324
|
+
out._meta_data && typeof out._meta_data === 'object'
|
|
325
|
+
? { ...(out._meta_data as Record<string, unknown>) }
|
|
326
|
+
: null;
|
|
327
|
+
for (const [fieldName, permission] of READ_PERMISSION_FIELDS) {
|
|
328
|
+
if (hasReadPermission(options, permission)) continue;
|
|
329
|
+
delete out[fieldName];
|
|
330
|
+
if (metaData) delete metaData[fieldName];
|
|
331
|
+
}
|
|
332
|
+
if (metaData) out._meta_data = metaData;
|
|
333
|
+
for (const [key, entry] of Object.entries(out)) {
|
|
334
|
+
out[key] = applyReadPermissionRedaction(entry, options, seen);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
218
339
|
// Sensitive-field-safe serialization for custom-action results (#1540): a
|
|
219
340
|
// custom method may return a SmrtObject (or one nested in an array/plain
|
|
220
341
|
// object), so recurse and route each through toPublicJSON() rather than letting
|
|
221
342
|
// JSON.stringify call toJSON(). Non-plain instances (Date, etc.) and primitives
|
|
222
343
|
// pass through; a cycle guard prevents infinite loops.
|
|
223
344
|
interface PublicJsonSource {
|
|
224
|
-
toPublicJSON(): unknown;
|
|
345
|
+
toPublicJSON(options?: PublicJsonOptions): unknown;
|
|
225
346
|
}
|
|
226
347
|
|
|
227
348
|
function hasPublicJson(value: object): value is PublicJsonSource {
|
|
@@ -238,17 +359,18 @@ function readJsonRecord(value: unknown): Record<string, unknown> {
|
|
|
238
359
|
|
|
239
360
|
function toPublicResult(
|
|
240
361
|
value: unknown,
|
|
362
|
+
options: PublicJsonOptions,
|
|
241
363
|
seen: WeakSet<object> = new WeakSet(),
|
|
242
364
|
): unknown {
|
|
243
365
|
if (value === null || typeof value !== 'object') return value;
|
|
244
366
|
if (seen.has(value)) return null;
|
|
245
367
|
if (hasPublicJson(value)) {
|
|
246
368
|
seen.add(value);
|
|
247
|
-
return toPublicResult(value.toPublicJSON(), seen);
|
|
369
|
+
return toPublicResult(value.toPublicJSON(options), options, seen);
|
|
248
370
|
}
|
|
249
371
|
if (Array.isArray(value)) {
|
|
250
372
|
seen.add(value);
|
|
251
|
-
return value.map((entry) => toPublicResult(entry, seen));
|
|
373
|
+
return value.map((entry) => toPublicResult(entry, options, seen));
|
|
252
374
|
}
|
|
253
375
|
const proto = Object.getPrototypeOf(value);
|
|
254
376
|
if (proto !== Object.prototype && proto !== null) return value;
|
|
@@ -257,7 +379,7 @@ function toPublicResult(
|
|
|
257
379
|
for (const [key, entry] of Object.entries(
|
|
258
380
|
value as Record<string, unknown>,
|
|
259
381
|
)) {
|
|
260
|
-
out[key] = toPublicResult(entry, seen);
|
|
382
|
+
out[key] = toPublicResult(entry, options, seen);
|
|
261
383
|
}
|
|
262
384
|
return out;
|
|
263
385
|
}
|
|
@@ -293,7 +415,7 @@ function isTenantScoped(objectDef) {
|
|
|
293
415
|
*/
|
|
294
416
|
function generateTenantContextHelper() {
|
|
295
417
|
return `
|
|
296
|
-
import { enterTenantContext, hasTenantContext, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
|
|
418
|
+
import { enterTenantContext, getCurrentTenant, hasTenantContext, isSuperAdminBypass, isTenancyEnabled } from '@happyvertical/smrt-tenancy';
|
|
297
419
|
|
|
298
420
|
function establishTenantContext(locals: unknown): void {
|
|
299
421
|
if (hasTenantContext()) return;
|
|
@@ -319,6 +441,18 @@ function tenantReadScope(): { tenantId: null } | undefined {
|
|
|
319
441
|
? { tenantId: null }
|
|
320
442
|
: undefined;
|
|
321
443
|
}
|
|
444
|
+
|
|
445
|
+
// Custom GET actions that accept a single options object may perform their
|
|
446
|
+
// own reads instead of calling generated list/get handlers. Give those actions
|
|
447
|
+
// an explicit tenant scope: active tenant when present, NULL-tenant global rows
|
|
448
|
+
// when tenancy is enabled but no tenant context exists. Super-admin bypass
|
|
449
|
+
// stays unscoped so admin callers keep deliberate cross-tenant access.
|
|
450
|
+
function tenantReadOptionsScope(): { tenantId: string | null } | undefined {
|
|
451
|
+
if (!isTenancyEnabled() || isSuperAdminBypass()) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
return { tenantId: getCurrentTenant()?.tenantId ?? null };
|
|
455
|
+
}
|
|
322
456
|
`;
|
|
323
457
|
}
|
|
324
458
|
/**
|
|
@@ -388,10 +522,14 @@ function buildPathParamsObjectLiteral(pathParamNames) {
|
|
|
388
522
|
${pathParamNames.map((paramName) => ` ${buildObjectTypeProperty(paramName)}: ${buildRouteParamAccess(paramName)},`).join("\n")}
|
|
389
523
|
}`;
|
|
390
524
|
}
|
|
391
|
-
function
|
|
525
|
+
function hasSingleOptionsParameter(actionDef) {
|
|
526
|
+
const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
|
|
527
|
+
return parameters.length === 1 && parameters[0]?.name === "options";
|
|
528
|
+
}
|
|
529
|
+
function buildActionInvocationArgs(actionDef, optionsIdentifier = "options") {
|
|
392
530
|
const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
|
|
393
531
|
if (parameters.length === 0) return [];
|
|
394
|
-
if (
|
|
532
|
+
if (hasSingleOptionsParameter(actionDef)) return [optionsIdentifier];
|
|
395
533
|
return parameters.map((parameter) => buildOptionsPropertyAccess(parameter.name));
|
|
396
534
|
}
|
|
397
535
|
function buildObjectTypeProperty(propertyName) {
|
|
@@ -456,6 +594,21 @@ function buildActionInvocationExpression(receiverExpression, actionName, invocat
|
|
|
456
594
|
" )"
|
|
457
595
|
].join("\n");
|
|
458
596
|
}
|
|
597
|
+
function buildScopedOptionsForTenantRead(actionDef, routeConfig, tenantScoped) {
|
|
598
|
+
if (!tenantScoped || routeConfig.method !== "GET" || !hasSingleOptionsParameter(actionDef)) return {
|
|
599
|
+
source: "",
|
|
600
|
+
optionsIdentifier: "options"
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
source: ` const readScope = tenantReadOptionsScope();
|
|
604
|
+
const scopedOptions = readScope
|
|
605
|
+
? ({ ...options, ...readScope } as ActionArgs[0])
|
|
606
|
+
: options;
|
|
607
|
+
|
|
608
|
+
`,
|
|
609
|
+
optionsIdentifier: "scopedOptions"
|
|
610
|
+
};
|
|
611
|
+
}
|
|
459
612
|
function findItemClassRegistryKey(className, objectDef, manifest) {
|
|
460
613
|
const itemClassName = objectDef.extendsTypeArg || (className.endsWith("Collection") ? className.slice(0, -10) : void 0);
|
|
461
614
|
if (!itemClassName) return className;
|
|
@@ -492,12 +645,13 @@ async function generateSvelteKitRoutes(projectRoot, manifest, options) {
|
|
|
492
645
|
}
|
|
493
646
|
continue;
|
|
494
647
|
}
|
|
495
|
-
await generateRoutesForObject(projectRoot, className, objectDef, options);
|
|
648
|
+
await generateRoutesForObject(projectRoot, className, objectDef, manifest, options);
|
|
496
649
|
generatedCount++;
|
|
497
650
|
}
|
|
498
651
|
if (options.knowledge?.api?.enabled) generateKnowledgeRoute(projectRoot, options);
|
|
499
652
|
generateSyncApplyRoute(projectRoot, manifest, options);
|
|
500
653
|
generateChangesRoute(projectRoot, manifest, options);
|
|
654
|
+
generateEventsRoute(projectRoot, manifest, options);
|
|
501
655
|
updateGitignore(projectRoot, options);
|
|
502
656
|
const skippedMsg = skippedCollections > 0 ? ` (skipped ${skippedCollections} collection classes)` : "";
|
|
503
657
|
console.log(`[smrt] Generated routes for ${generatedCount} SMRT objects${skippedMsg}`);
|
|
@@ -780,7 +934,7 @@ export async function getCollection<
|
|
|
780
934
|
/**
|
|
781
935
|
* Generates route files for a single SMRT object
|
|
782
936
|
*/
|
|
783
|
-
async function generateRoutesForObject(projectRoot, className, objectDef, options) {
|
|
937
|
+
async function generateRoutesForObject(projectRoot, className, objectDef, manifest, options) {
|
|
784
938
|
const collectionName = objectDef.collection;
|
|
785
939
|
const routeDir = join(projectRoot, options.routesDir, collectionName);
|
|
786
940
|
const apiConfig = objectDef.decoratorConfig?.api;
|
|
@@ -789,9 +943,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
789
943
|
return;
|
|
790
944
|
}
|
|
791
945
|
const includedActions = resolveStandardCrudActions(apiConfig);
|
|
792
|
-
if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir));
|
|
946
|
+
if (includedActions.includes("list") || includedActions.includes("create")) writeRoute(routeDir, "+server.ts", generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir));
|
|
793
947
|
if (includedActions.includes("get") || includedActions.includes("update") || includedActions.includes("delete")) {
|
|
794
|
-
const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, join(routeDir, "[id]"));
|
|
948
|
+
const itemRoute = generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, join(routeDir, "[id]"));
|
|
795
949
|
writeRoute(join(routeDir, "[id]"), "+server.ts", itemRoute);
|
|
796
950
|
}
|
|
797
951
|
const customActions = Object.entries(objectDef.methods).filter(([name, method]) => !STANDARD_API_ACTIONS.includes(name) && method.isPublic && shouldIncludeInApi(name, apiConfig));
|
|
@@ -816,7 +970,7 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
816
970
|
}
|
|
817
971
|
});
|
|
818
972
|
}
|
|
819
|
-
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
|
|
973
|
+
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
|
|
820
974
|
}
|
|
821
975
|
async function generateCollectionRoutesForObject(projectRoot, className, objectDef, manifest, options) {
|
|
822
976
|
const apiConfig = objectDef.decoratorConfig?.api;
|
|
@@ -851,7 +1005,7 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
|
|
|
851
1005
|
});
|
|
852
1006
|
}
|
|
853
1007
|
for (const [actionRouteDir, routeSpecs] of groupCustomActionRoutes(actionSpecs)) {
|
|
854
|
-
writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, options));
|
|
1008
|
+
writeRoute(actionRouteDir, "+server.ts", generateActionRouteTemplate(projectRoot, actionRouteDir, routeSpecs, objectDef, manifest, options));
|
|
855
1009
|
generatedAnyRoutes = true;
|
|
856
1010
|
}
|
|
857
1011
|
return generatedAnyRoutes;
|
|
@@ -998,13 +1152,15 @@ function isLocalObject(projectRoot, objectDef) {
|
|
|
998
1152
|
/**
|
|
999
1153
|
* Generates collection route template (GET list, POST create)
|
|
1000
1154
|
*/
|
|
1001
|
-
function generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
1155
|
+
function generateCollectionRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
|
|
1002
1156
|
const hasGet = includedActions.includes("list");
|
|
1003
1157
|
const hasPost = includedActions.includes("create");
|
|
1004
1158
|
const modelType = resolveObjectTypeReference(projectRoot, className, objectDef, options, routeDir);
|
|
1005
1159
|
const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
|
|
1006
1160
|
const serializerImports = serializers.importStatements.join("\n");
|
|
1007
1161
|
const listUsesSerializer = !!serializers.listItemSerializerName;
|
|
1162
|
+
const listUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
|
|
1163
|
+
const listUsesBodyHash = listUsesSerializer || listUsesPermissionScopedBody;
|
|
1008
1164
|
const imports = `${AUTO_GENERATED_ROUTE_HEADER}
|
|
1009
1165
|
// DO NOT EDIT - changes will be overwritten
|
|
1010
1166
|
|
|
@@ -1012,10 +1168,11 @@ import { error${hasPost ? ", json" : ""} } from '@sveltejs/kit';
|
|
|
1012
1168
|
${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
|
|
1013
1169
|
${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
|
|
1014
1170
|
// Note: ${className} is auto-registered by the Vite plugin scanner
|
|
1015
|
-
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1171
|
+
${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1016
1172
|
tenantScoped: isTenantScoped(objectDef),
|
|
1173
|
+
permissionScoped: listUsesPermissionScopedBody,
|
|
1017
1174
|
modelName: className,
|
|
1018
|
-
useBodyHash:
|
|
1175
|
+
useBodyHash: listUsesBodyHash
|
|
1019
1176
|
}) : ""}`;
|
|
1020
1177
|
const listAndCount = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1021
1178
|
const items = await collection.list({ limit, offset, where: readScope });
|
|
@@ -1025,6 +1182,7 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
1025
1182
|
// List all ${className.toLowerCase()}s
|
|
1026
1183
|
export const GET: RequestHandler = async ({ locals, url, request }) => {
|
|
1027
1184
|
${routeGuardPreamble(objectDef, false)}
|
|
1185
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1028
1186
|
const limit = Number(url.searchParams.get('limit')) || 50;
|
|
1029
1187
|
const offset = Number(url.searchParams.get('offset')) || 0;
|
|
1030
1188
|
|
|
@@ -1032,13 +1190,21 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
|
1032
1190
|
${listUsesSerializer ? `${listAndCount}
|
|
1033
1191
|
// Custom serializer may render related-table data → v1 body-hash ETag (#1765).
|
|
1034
1192
|
const serializedItems = await Promise.all(
|
|
1035
|
-
items.map((item) =>
|
|
1193
|
+
items.map(async (item) =>
|
|
1194
|
+
applyReadPermissionRedaction(
|
|
1195
|
+
await ${serializers.listItemSerializerName}(item),
|
|
1196
|
+
publicJsonOptions,
|
|
1197
|
+
),
|
|
1198
|
+
),
|
|
1036
1199
|
);
|
|
1037
|
-
return conditionalJson(request, { items: serializedItems, count, limit, offset });` :
|
|
1200
|
+
return conditionalJson(request, { items: serializedItems, count, limit, offset });` : listUsesPermissionScopedBody ? `${listAndCount}
|
|
1201
|
+
// Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
|
|
1202
|
+
const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
|
|
1203
|
+
return conditionalJson(request, { items: items_public, count, limit, offset });` : ` // ETag v2 (#1765): the table-version ETag is checked first; on a concrete
|
|
1038
1204
|
// If-None-Match the list query below never runs (zero-query 304).
|
|
1039
1205
|
return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
|
|
1040
1206
|
${listAndCount}
|
|
1041
|
-
const items_public = items.map((item) => item.toPublicJSON());
|
|
1207
|
+
const items_public = items.map((item) => item.toPublicJSON(publicJsonOptions));
|
|
1042
1208
|
return { items: items_public, count, limit, offset };
|
|
1043
1209
|
});`}
|
|
1044
1210
|
};
|
|
@@ -1047,6 +1213,7 @@ ${listAndCount}
|
|
|
1047
1213
|
// Create new ${className.toLowerCase()}
|
|
1048
1214
|
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
1049
1215
|
${routeGuardPreamble(objectDef, true)}
|
|
1216
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1050
1217
|
const body: unknown = await request.json();
|
|
1051
1218
|
const data = applyWritablePolicy(body);
|
|
1052
1219
|
|
|
@@ -1056,8 +1223,11 @@ ${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
|
1056
1223
|
${serializers.itemSerializerName ? `
|
|
1057
1224
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1058
1225
|
|
|
1059
|
-
return json(
|
|
1060
|
-
|
|
1226
|
+
return json(
|
|
1227
|
+
applyReadPermissionRedaction(serializedItem, publicJsonOptions),
|
|
1228
|
+
{ status: 201 },
|
|
1229
|
+
);` : `
|
|
1230
|
+
return json(item.toPublicJSON(publicJsonOptions), { status: 201 });`}
|
|
1061
1231
|
};
|
|
1062
1232
|
` : "";
|
|
1063
1233
|
return imports + getHandler + postHandler;
|
|
@@ -1101,7 +1271,7 @@ function generateClassNotRegisteredError(className) {
|
|
|
1101
1271
|
/**
|
|
1102
1272
|
* Generates item route template (GET, PUT, DELETE)
|
|
1103
1273
|
*/
|
|
1104
|
-
function generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
1274
|
+
function generateItemRouteTemplate(projectRoot, className, objectDef, manifest, includedActions, options, routeDir) {
|
|
1105
1275
|
const hasGet = includedActions.includes("get");
|
|
1106
1276
|
const hasPut = includedActions.includes("update");
|
|
1107
1277
|
const hasDelete = includedActions.includes("delete");
|
|
@@ -1110,16 +1280,19 @@ function generateItemRouteTemplate(projectRoot, className, objectDef, includedAc
|
|
|
1110
1280
|
const serializers = resolveStandardRouteSerializers(objectDef.decoratorConfig?.api);
|
|
1111
1281
|
const serializerImports = serializers.importStatements.join("\n");
|
|
1112
1282
|
const getUsesSerializer = !!serializers.itemSerializerName;
|
|
1283
|
+
const getUsesPermissionScopedBody = collectReadPermissionFields(objectDef, manifest).length > 0;
|
|
1284
|
+
const getUsesBodyHash = getUsesSerializer || getUsesPermissionScopedBody;
|
|
1113
1285
|
const imports = `${AUTO_GENERATED_ROUTE_HEADER}
|
|
1114
1286
|
// DO NOT EDIT - changes will be overwritten
|
|
1115
1287
|
|
|
1116
1288
|
import { error${hasPut || hasDelete ? ", json" : ""} } from '@sveltejs/kit';
|
|
1117
1289
|
${serializerImports ? `${serializerImports}\n` : ""}import { getCollection } from '$lib/server/smrt';
|
|
1118
1290
|
${modelType.importStatement ? `${modelType.importStatement}\n` : ""}import type { RequestHandler } from './$types';
|
|
1119
|
-
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1291
|
+
${generateAuthGuardHelper(objectDef, manifest)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}${hasGet ? generateConditionalGetRouteHelper(objectDef.decoratorConfig?.api, {
|
|
1120
1292
|
tenantScoped: isTenantScoped(objectDef),
|
|
1293
|
+
permissionScoped: getUsesPermissionScopedBody,
|
|
1121
1294
|
modelName: className,
|
|
1122
|
-
useBodyHash:
|
|
1295
|
+
useBodyHash: getUsesBodyHash
|
|
1123
1296
|
}) : ""}`;
|
|
1124
1297
|
const getForRead = isTenantScoped(objectDef) ? ` const readScope = tenantReadScope();
|
|
1125
1298
|
const item = await collection.get(
|
|
@@ -1129,18 +1302,25 @@ ${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenan
|
|
|
1129
1302
|
// Get single ${simpleClassName.toLowerCase()}
|
|
1130
1303
|
export const GET: RequestHandler = async ({ locals, params, request }) => {
|
|
1131
1304
|
${routeGuardPreamble(objectDef, false)}
|
|
1305
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1132
1306
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1133
1307
|
${getUsesSerializer ? `${getForRead}
|
|
1134
1308
|
${generateNotFoundError(className)}
|
|
1135
1309
|
// Custom serializer may render related-table data → v1 body-hash ETag (#1765).
|
|
1136
1310
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1137
|
-
return conditionalJson(
|
|
1311
|
+
return conditionalJson(
|
|
1312
|
+
request,
|
|
1313
|
+
applyReadPermissionRedaction(serializedItem, publicJsonOptions),
|
|
1314
|
+
);` : getUsesPermissionScopedBody ? `${getForRead}
|
|
1315
|
+
${generateNotFoundError(className)}
|
|
1316
|
+
// Field read permissions make the body vary by caller permissions → v1 body-hash ETag (#1565).
|
|
1317
|
+
return conditionalJson(request, item.toPublicJSON(publicJsonOptions));` : ` // ETag v2 (#1765): a concrete If-None-Match answers 304 without the row fetch
|
|
1138
1318
|
// (a delete advances the version, so a since-deleted row can't false-match);
|
|
1139
1319
|
// a wildcard \`*\` is deferred until the fetch confirms the row exists.
|
|
1140
1320
|
return conditionalVersionedRead(request, collection.db, collection.tableName, async () => {
|
|
1141
1321
|
${getForRead}
|
|
1142
1322
|
${generateNotFoundError(className)}
|
|
1143
|
-
return item.toPublicJSON();
|
|
1323
|
+
return item.toPublicJSON(publicJsonOptions);
|
|
1144
1324
|
});`}
|
|
1145
1325
|
};
|
|
1146
1326
|
` : "";
|
|
@@ -1148,6 +1328,7 @@ ${generateNotFoundError(className)}
|
|
|
1148
1328
|
// Update ${simpleClassName.toLowerCase()}
|
|
1149
1329
|
export const PUT: RequestHandler = async ({ locals, params, request }) => {
|
|
1150
1330
|
${routeGuardPreamble(objectDef, true)}
|
|
1331
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1151
1332
|
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1152
1333
|
const item = await collection.get(params.id);
|
|
1153
1334
|
${generateNotFoundError(className)}
|
|
@@ -1159,8 +1340,8 @@ ${generateNotFoundError(className)}
|
|
|
1159
1340
|
${serializers.itemSerializerName ? `
|
|
1160
1341
|
const serializedItem = await ${serializers.itemSerializerName}(item);
|
|
1161
1342
|
|
|
1162
|
-
return json(serializedItem);` : `
|
|
1163
|
-
return json(item.toPublicJSON());`}
|
|
1343
|
+
return json(applyReadPermissionRedaction(serializedItem, publicJsonOptions));` : `
|
|
1344
|
+
return json(item.toPublicJSON(publicJsonOptions));`}
|
|
1164
1345
|
};
|
|
1165
1346
|
` : "";
|
|
1166
1347
|
const deleteHandler = hasDelete ? `
|
|
@@ -1180,7 +1361,7 @@ ${generateNotFoundError(className)}
|
|
|
1180
1361
|
/**
|
|
1181
1362
|
* Generates custom action route template
|
|
1182
1363
|
*/
|
|
1183
|
-
function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, options) {
|
|
1364
|
+
function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, manifest, options) {
|
|
1184
1365
|
if (routeSpecs.length === 0) throw new Error("Cannot generate a custom action route without handlers");
|
|
1185
1366
|
const [firstSpec] = routeSpecs;
|
|
1186
1367
|
const { lookupClassName, routeConfig, hostType } = firstSpec;
|
|
@@ -1200,7 +1381,7 @@ function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDe
|
|
|
1200
1381
|
// DO NOT EDIT - changes will be overwritten
|
|
1201
1382
|
|
|
1202
1383
|
${importBlock}
|
|
1203
|
-
${generateAuthGuardHelper(objectDef)}${tenantScoped ? generateTenantContextHelper() : ""}
|
|
1384
|
+
${generateAuthGuardHelper(objectDef, manifest)}${tenantScoped ? generateTenantContextHelper() : ""}
|
|
1204
1385
|
${handlers}`;
|
|
1205
1386
|
}
|
|
1206
1387
|
function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
|
|
@@ -1209,53 +1390,73 @@ function generateActionRouteHandler(lookupClassName, actionName, actionDef, rout
|
|
|
1209
1390
|
if (tenantScoped) guardLines.push(" establishTenantContext(locals);");
|
|
1210
1391
|
const authGuardLine = guardLines.join("\n");
|
|
1211
1392
|
const needsRequest = actionDef.parameters.length > 0;
|
|
1212
|
-
const invocationArgs = buildActionInvocationArgs(actionDef);
|
|
1213
1393
|
const collectionHandlerArgs = buildRouteHandlerArgs(routeConfig.pathParamNames.length > 0, needsRequest);
|
|
1214
1394
|
const itemHandlerArgs = buildRouteHandlerArgs(true, needsRequest);
|
|
1215
1395
|
if (hostType === "collection") {
|
|
1216
1396
|
const optionsLoad = buildActionOptionsLoad(actionName, actionDef, routeConfig, hostTypeName, actionDef.isStatic);
|
|
1217
1397
|
const receiverExpression = actionDef.isStatic ? "CollectionClass" : "typedCollection";
|
|
1218
1398
|
const staticTargetLoad = actionDef.isStatic ? ` const CollectionClass = typedCollection.constructor as typeof ${hostTypeName};\n` : "";
|
|
1399
|
+
const scopedOptions = buildScopedOptionsForTenantRead(actionDef, routeConfig, tenantScoped);
|
|
1400
|
+
const invocationArgs = buildActionInvocationArgs(actionDef, scopedOptions.optionsIdentifier);
|
|
1219
1401
|
return `// Custom collection method: ${actionName}
|
|
1220
1402
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1221
1403
|
${authGuardLine}
|
|
1404
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1222
1405
|
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1223
1406
|
const typedCollection = collection as unknown as ${hostTypeName};
|
|
1224
1407
|
${generateCollectionNotRegisteredError(lookupClassName)}
|
|
1225
1408
|
${staticTargetLoad}
|
|
1226
1409
|
|
|
1227
|
-
${optionsLoad} const result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
|
|
1410
|
+
${optionsLoad}${scopedOptions.source} const result = ${buildActionInvocationExpression(receiverExpression, actionName, invocationArgs)};
|
|
1228
1411
|
|
|
1229
|
-
return json({
|
|
1412
|
+
return json({
|
|
1413
|
+
action: '${actionName}',
|
|
1414
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1415
|
+
});
|
|
1230
1416
|
};
|
|
1231
1417
|
`;
|
|
1232
1418
|
}
|
|
1233
1419
|
if (routeConfig.scope === "collection") {
|
|
1234
1420
|
const optionsLoad = buildActionOptionsLoad(actionName, actionDef, routeConfig, hostTypeName, true);
|
|
1421
|
+
const scopedOptions = buildScopedOptionsForTenantRead(actionDef, routeConfig, tenantScoped);
|
|
1422
|
+
const invocationArgs = buildActionInvocationArgs(actionDef, scopedOptions.optionsIdentifier);
|
|
1235
1423
|
return `// Custom collection action: ${actionName}
|
|
1236
1424
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1237
1425
|
${authGuardLine}
|
|
1426
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1238
1427
|
const registered = ObjectRegistry.getClass('${lookupClassName}');
|
|
1239
1428
|
${generateClassNotRegisteredError(lookupClassName)}
|
|
1240
1429
|
|
|
1241
|
-
${optionsLoad} const ClassRef = registered.constructor as typeof ${hostTypeName};
|
|
1430
|
+
${optionsLoad}${scopedOptions.source} const ClassRef = registered.constructor as typeof ${hostTypeName};
|
|
1242
1431
|
const result = ${buildActionInvocationExpression("ClassRef", actionName, invocationArgs)};
|
|
1243
1432
|
|
|
1244
|
-
return json({
|
|
1433
|
+
return json({
|
|
1434
|
+
action: '${actionName}',
|
|
1435
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1436
|
+
});
|
|
1245
1437
|
};
|
|
1246
1438
|
`;
|
|
1247
1439
|
}
|
|
1248
1440
|
const optionsLoad = buildActionOptionsLoad(actionName, actionDef, routeConfig, hostTypeName, false);
|
|
1441
|
+
const scopedItemLoad = tenantScoped && routeConfig.method === "GET" ? ` const readScope = tenantReadScope();
|
|
1442
|
+
const item = await collection.get(
|
|
1443
|
+
readScope ? { id: params.id, ...readScope } : params.id,
|
|
1444
|
+
);` : " const item = await collection.get(params.id);";
|
|
1445
|
+
const invocationArgs = buildActionInvocationArgs(actionDef);
|
|
1249
1446
|
return `// Custom action: ${actionName}
|
|
1250
1447
|
export const ${handlerName}: RequestHandler = async (${itemHandlerArgs}) => {
|
|
1251
1448
|
${authGuardLine}
|
|
1449
|
+
const publicJsonOptions = getPublicJsonOptions(locals);
|
|
1252
1450
|
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1253
|
-
|
|
1451
|
+
${scopedItemLoad}
|
|
1254
1452
|
${generateNotFoundError(lookupClassName)}
|
|
1255
1453
|
|
|
1256
1454
|
${optionsLoad} const result = ${buildActionInvocationExpression("item", actionName, invocationArgs)};
|
|
1257
1455
|
|
|
1258
|
-
return json({
|
|
1456
|
+
return json({
|
|
1457
|
+
action: '${actionName}',
|
|
1458
|
+
result: toPublicResult(result, publicJsonOptions),
|
|
1459
|
+
});
|
|
1259
1460
|
};
|
|
1260
1461
|
`;
|
|
1261
1462
|
}
|