@happyvertical/smrt-core 0.36.0 → 0.36.1
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/manifest/static-manifest.js +2 -2
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +2 -2
- package/dist/manifest/test-manifest-stub.js +27605 -27605
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/smrt-knowledge.json +4 -4
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +304 -80
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/package.json +4 -4
|
@@ -62,6 +62,53 @@ function resolveStandardRouteSerializers(apiConfig) {
|
|
|
62
62
|
listItemSerializerName
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
function isValidTypeIdentifier(name) {
|
|
66
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
67
|
+
}
|
|
68
|
+
function resolveObjectTypeReference(projectRoot, className, objectDef, options, routeDir) {
|
|
69
|
+
const simpleClassName = extractSimpleClassName(className);
|
|
70
|
+
if (!objectDef || !isValidTypeIdentifier(simpleClassName)) {
|
|
71
|
+
return { typeName: "import('@happyvertical/smrt-core').SmrtObject" };
|
|
72
|
+
}
|
|
73
|
+
const importPath = isLocalObject(projectRoot, objectDef) || !objectDef.packageName ? getRouteTypeImportPath(
|
|
74
|
+
projectRoot,
|
|
75
|
+
objectDef,
|
|
76
|
+
options,
|
|
77
|
+
simpleClassName,
|
|
78
|
+
routeDir
|
|
79
|
+
) : objectDef.packageName;
|
|
80
|
+
return {
|
|
81
|
+
typeName: simpleClassName,
|
|
82
|
+
importStatement: `import type { ${simpleClassName} } from '${importPath}';`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getRouteTypeImportPath(projectRoot, objectDef, options, simpleClassName, routeDir) {
|
|
86
|
+
if (objectDef.filePath) {
|
|
87
|
+
const srcLibDir = join(projectRoot, "src/lib");
|
|
88
|
+
if (objectDef.filePath.startsWith(`${srcLibDir}/`)) {
|
|
89
|
+
const libRelative = relative(srcLibDir, objectDef.filePath).replace(/\\/g, "/").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
90
|
+
return `$lib/${libRelative}`.replace(/\/+/g, "/");
|
|
91
|
+
}
|
|
92
|
+
if (routeDir) {
|
|
93
|
+
const routeRelative = relative(routeDir, objectDef.filePath).replace(/\\/g, "/").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
94
|
+
return routeRelative.startsWith(".") ? routeRelative : `./${routeRelative}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return getSvelteKitImportPath(
|
|
98
|
+
projectRoot,
|
|
99
|
+
objectDef.filePath,
|
|
100
|
+
options.objectsDir,
|
|
101
|
+
simpleClassName
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function mergeImportStatements(importStatements) {
|
|
105
|
+
return Array.from(new Set(importStatements.filter(Boolean))).join("\n");
|
|
106
|
+
}
|
|
107
|
+
function findObjectDefByRegistryKey(manifest, className) {
|
|
108
|
+
return manifest.objects[className] ?? Object.values(manifest.objects).find(
|
|
109
|
+
(objectDef) => objectDef.className === extractSimpleClassName(className)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
65
112
|
function collectReadonlyFieldNames(objectDef) {
|
|
66
113
|
const fields = objectDef.fields || {};
|
|
67
114
|
const names = [];
|
|
@@ -155,20 +202,43 @@ function requireRouteAuth(locals: unknown, mutating: boolean): void {
|
|
|
155
202
|
// object), so recurse and route each through toPublicJSON() rather than letting
|
|
156
203
|
// JSON.stringify call toJSON(). Non-plain instances (Date, etc.) and primitives
|
|
157
204
|
// pass through; a cycle guard prevents infinite loops.
|
|
158
|
-
|
|
205
|
+
interface PublicJsonSource {
|
|
206
|
+
toPublicJSON(): unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasPublicJson(value: object): value is PublicJsonSource {
|
|
210
|
+
return (
|
|
211
|
+
'toPublicJSON' in value &&
|
|
212
|
+
typeof (value as { toPublicJSON?: unknown }).toPublicJSON === 'function'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readJsonRecord(value: unknown): Record<string, unknown> {
|
|
217
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
218
|
+
return value as Record<string, unknown>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function toPublicResult(
|
|
222
|
+
value: unknown,
|
|
223
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
224
|
+
): unknown {
|
|
159
225
|
if (value === null || typeof value !== 'object') return value;
|
|
160
|
-
if (
|
|
226
|
+
if (seen.has(value)) return null;
|
|
227
|
+
if (hasPublicJson(value)) {
|
|
228
|
+
seen.add(value);
|
|
229
|
+
return toPublicResult(value.toPublicJSON(), seen);
|
|
230
|
+
}
|
|
161
231
|
if (Array.isArray(value)) {
|
|
162
|
-
if (seen.has(value)) return value;
|
|
163
232
|
seen.add(value);
|
|
164
|
-
return value.map((entry
|
|
233
|
+
return value.map((entry) => toPublicResult(entry, seen));
|
|
165
234
|
}
|
|
166
235
|
const proto = Object.getPrototypeOf(value);
|
|
167
236
|
if (proto !== Object.prototype && proto !== null) return value;
|
|
168
|
-
if (seen.has(value)) return value;
|
|
169
237
|
seen.add(value);
|
|
170
|
-
const out: Record<string,
|
|
171
|
-
for (const [key, entry] of Object.entries(
|
|
238
|
+
const out: Record<string, unknown> = {};
|
|
239
|
+
for (const [key, entry] of Object.entries(
|
|
240
|
+
value as Record<string, unknown>,
|
|
241
|
+
)) {
|
|
172
242
|
out[key] = toPublicResult(entry, seen);
|
|
173
243
|
}
|
|
174
244
|
return out;
|
|
@@ -185,8 +255,10 @@ import { enterTenantContext, hasTenantContext } from '@happyvertical/smrt-tenanc
|
|
|
185
255
|
function establishTenantContext(locals: unknown): void {
|
|
186
256
|
if (hasTenantContext()) return;
|
|
187
257
|
if (!locals || typeof locals !== 'object') return;
|
|
188
|
-
const l = locals as Record<string,
|
|
189
|
-
const
|
|
258
|
+
const l = locals as Record<string, unknown>;
|
|
259
|
+
const user = l.user as Record<string, unknown> | undefined;
|
|
260
|
+
const session = l.session as Record<string, unknown> | undefined;
|
|
261
|
+
const tenantId = l.tenantId ?? user?.tenantId ?? session?.tenantId;
|
|
190
262
|
if (typeof tenantId === 'string' && tenantId) {
|
|
191
263
|
enterTenantContext({ tenantId });
|
|
192
264
|
}
|
|
@@ -275,6 +347,92 @@ function buildActionInvocationArgs(actionDef) {
|
|
|
275
347
|
(parameter) => buildOptionsPropertyAccess(parameter.name)
|
|
276
348
|
);
|
|
277
349
|
}
|
|
350
|
+
function buildObjectTypeProperty(propertyName) {
|
|
351
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyName)) {
|
|
352
|
+
return propertyName;
|
|
353
|
+
}
|
|
354
|
+
return JSON.stringify(propertyName);
|
|
355
|
+
}
|
|
356
|
+
function buildActionTargetTypeExpression(targetTypeName, isStatic) {
|
|
357
|
+
return isStatic ? `(typeof ${targetTypeName})` : targetTypeName;
|
|
358
|
+
}
|
|
359
|
+
function buildActionArgsTypeAlias(actionName, targetTypeName, isStatic) {
|
|
360
|
+
const targetTypeExpression = buildActionTargetTypeExpression(
|
|
361
|
+
targetTypeName,
|
|
362
|
+
isStatic
|
|
363
|
+
);
|
|
364
|
+
return ` type ActionArgs = Parameters<${targetTypeExpression}[${JSON.stringify(actionName)}]>;`;
|
|
365
|
+
}
|
|
366
|
+
function buildActionOptionsTypeAlias(actionDef) {
|
|
367
|
+
const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
|
|
368
|
+
if (parameters.length <= 1 && parameters[0]?.name === "options") {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
return [
|
|
372
|
+
" type ActionOptions = {",
|
|
373
|
+
...parameters.map(
|
|
374
|
+
(parameter, index) => ` ${buildObjectTypeProperty(parameter.name)}: ActionArgs[${index}];`
|
|
375
|
+
),
|
|
376
|
+
" };"
|
|
377
|
+
].join("\n");
|
|
378
|
+
}
|
|
379
|
+
function buildActionOptionsLoad(actionName, actionDef, routeConfig, targetTypeName, isStatic) {
|
|
380
|
+
const parameters = Array.isArray(actionDef.parameters) ? actionDef.parameters : [];
|
|
381
|
+
if (parameters.length === 0) {
|
|
382
|
+
return "";
|
|
383
|
+
}
|
|
384
|
+
const hasPathParams = routeConfig.pathParamNames.length > 0;
|
|
385
|
+
const pathParamsObjectLiteral = buildPathParamsObjectLiteral(
|
|
386
|
+
routeConfig.pathParamNames
|
|
387
|
+
);
|
|
388
|
+
const isSingleOptionsParameter = parameters.length === 1 && parameters[0]?.name === "options";
|
|
389
|
+
const lines = [
|
|
390
|
+
buildActionArgsTypeAlias(actionName, targetTypeName, isStatic)
|
|
391
|
+
];
|
|
392
|
+
const optionsTypeAlias = buildActionOptionsTypeAlias(actionDef);
|
|
393
|
+
if (optionsTypeAlias) {
|
|
394
|
+
lines.push(optionsTypeAlias);
|
|
395
|
+
}
|
|
396
|
+
if (routeConfig.method === "GET") {
|
|
397
|
+
if (hasPathParams) {
|
|
398
|
+
lines.push(
|
|
399
|
+
` const pathParams = ${pathParamsObjectLiteral};`,
|
|
400
|
+
" const options = {",
|
|
401
|
+
" ...Object.fromEntries(new URL(request.url).searchParams.entries()),",
|
|
402
|
+
" ...pathParams,",
|
|
403
|
+
` } as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
|
|
404
|
+
""
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
lines.push(
|
|
408
|
+
" const options = Object.fromEntries(",
|
|
409
|
+
" new URL(request.url).searchParams.entries(),",
|
|
410
|
+
` ) as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
|
|
411
|
+
""
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}
|
|
416
|
+
if (hasPathParams) {
|
|
417
|
+
lines.push(
|
|
418
|
+
` const pathParams = ${pathParamsObjectLiteral};`,
|
|
419
|
+
" const body: unknown = await request.json();",
|
|
420
|
+
" const options = {",
|
|
421
|
+
" ...readJsonRecord(body),",
|
|
422
|
+
" ...pathParams,",
|
|
423
|
+
` } as ${isSingleOptionsParameter ? "ActionArgs[0]" : "ActionOptions"};`,
|
|
424
|
+
""
|
|
425
|
+
);
|
|
426
|
+
return lines.join("\n");
|
|
427
|
+
}
|
|
428
|
+
lines.push(" const body: unknown = await request.json();");
|
|
429
|
+
if (isSingleOptionsParameter) {
|
|
430
|
+
lines.push(" const options = body as ActionArgs[0];", "");
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(" const options = readJsonRecord(body) as ActionOptions;", "");
|
|
433
|
+
}
|
|
434
|
+
return lines.join("\n");
|
|
435
|
+
}
|
|
278
436
|
function buildOptionsPropertyAccess(propertyName) {
|
|
279
437
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyName)) {
|
|
280
438
|
return `options.${propertyName}`;
|
|
@@ -380,7 +538,9 @@ function clearGeneratedRouteFiles(routesRoot) {
|
|
|
380
538
|
}
|
|
381
539
|
}
|
|
382
540
|
function knowledgeRouteDir(projectRoot, options) {
|
|
383
|
-
const basePath =
|
|
541
|
+
const basePath = String(
|
|
542
|
+
options.knowledge?.api?.basePath || "/__smrt/knowledge"
|
|
543
|
+
);
|
|
384
544
|
const routeRoot = svelteKitRouteRoot(options.routesDir);
|
|
385
545
|
const segments = basePath.split("/").map((segment) => segment.trim()).filter(Boolean);
|
|
386
546
|
return join(projectRoot, routeRoot, ...segments);
|
|
@@ -710,7 +870,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
710
870
|
projectRoot,
|
|
711
871
|
className,
|
|
712
872
|
objectDef,
|
|
713
|
-
includedActions
|
|
873
|
+
includedActions,
|
|
874
|
+
options,
|
|
875
|
+
routeDir
|
|
714
876
|
);
|
|
715
877
|
writeRoute(routeDir, "+server.ts", collectionRoute);
|
|
716
878
|
}
|
|
@@ -719,7 +881,9 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
719
881
|
projectRoot,
|
|
720
882
|
className,
|
|
721
883
|
objectDef,
|
|
722
|
-
includedActions
|
|
884
|
+
includedActions,
|
|
885
|
+
options,
|
|
886
|
+
join(routeDir, "[id]")
|
|
723
887
|
);
|
|
724
888
|
writeRoute(join(routeDir, "[id]"), "+server.ts", itemRoute);
|
|
725
889
|
}
|
|
@@ -745,6 +909,8 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
745
909
|
routeDir: join(actionBaseDir, ...routeConfig.pathSegments),
|
|
746
910
|
spec: {
|
|
747
911
|
lookupClassName: className,
|
|
912
|
+
lookupObjectDef: objectDef,
|
|
913
|
+
hostClassName: className,
|
|
748
914
|
actionName,
|
|
749
915
|
actionDef,
|
|
750
916
|
routeConfig,
|
|
@@ -757,8 +923,10 @@ async function generateRoutesForObject(projectRoot, className, objectDef, option
|
|
|
757
923
|
)) {
|
|
758
924
|
const actionRoute = generateActionRouteTemplate(
|
|
759
925
|
projectRoot,
|
|
926
|
+
actionRouteDir,
|
|
760
927
|
routeSpecs,
|
|
761
|
-
objectDef
|
|
928
|
+
objectDef,
|
|
929
|
+
options
|
|
762
930
|
);
|
|
763
931
|
writeRoute(actionRouteDir, "+server.ts", actionRoute);
|
|
764
932
|
}
|
|
@@ -775,6 +943,7 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
|
|
|
775
943
|
objectDef,
|
|
776
944
|
manifest
|
|
777
945
|
);
|
|
946
|
+
const lookupObjectDef = findObjectDefByRegistryKey(manifest, lookupClassName);
|
|
778
947
|
const customActions = Object.entries(objectDef.methods).filter(
|
|
779
948
|
([name, method]) => !STANDARD_API_ACTIONS.includes(name) && method.isPublic && shouldIncludeInApi(name, apiConfig)
|
|
780
949
|
);
|
|
@@ -801,6 +970,8 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
|
|
|
801
970
|
routeDir: join(routeDir, ...routeConfig.pathSegments),
|
|
802
971
|
spec: {
|
|
803
972
|
lookupClassName,
|
|
973
|
+
lookupObjectDef,
|
|
974
|
+
hostClassName: className,
|
|
804
975
|
actionName,
|
|
805
976
|
actionDef,
|
|
806
977
|
routeConfig,
|
|
@@ -813,8 +984,10 @@ async function generateCollectionRoutesForObject(projectRoot, className, objectD
|
|
|
813
984
|
)) {
|
|
814
985
|
const actionRoute = generateActionRouteTemplate(
|
|
815
986
|
projectRoot,
|
|
987
|
+
actionRouteDir,
|
|
816
988
|
routeSpecs,
|
|
817
|
-
objectDef
|
|
989
|
+
objectDef,
|
|
990
|
+
options
|
|
818
991
|
);
|
|
819
992
|
writeRoute(actionRouteDir, "+server.ts", actionRoute);
|
|
820
993
|
generatedAnyRoutes = true;
|
|
@@ -922,7 +1095,7 @@ function writeRoute(dir, filename, content) {
|
|
|
922
1095
|
}
|
|
923
1096
|
}
|
|
924
1097
|
function getSvelteKitImportPath(projectRoot, objectFilePath, objectsDir, className) {
|
|
925
|
-
const filePath = join(projectRoot, objectsDir, `${className || "Object"}.ts`);
|
|
1098
|
+
const filePath = objectFilePath || join(projectRoot, objectsDir, `${className || "Object"}.ts`);
|
|
926
1099
|
const absoluteObjectsDir = objectsDir.startsWith("/") ? objectsDir : join(projectRoot, objectsDir);
|
|
927
1100
|
const relativePath = relative(absoluteObjectsDir, filePath);
|
|
928
1101
|
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
@@ -938,9 +1111,16 @@ function isLocalObject(projectRoot, objectDef) {
|
|
|
938
1111
|
if (!objectDef.filePath) return false;
|
|
939
1112
|
return objectDef.filePath.startsWith(projectRoot) && !objectDef.filePath.includes("/node_modules/");
|
|
940
1113
|
}
|
|
941
|
-
function generateCollectionRouteTemplate(
|
|
1114
|
+
function generateCollectionRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
942
1115
|
const hasGet = includedActions.includes("list");
|
|
943
1116
|
const hasPost = includedActions.includes("create");
|
|
1117
|
+
const modelType = resolveObjectTypeReference(
|
|
1118
|
+
projectRoot,
|
|
1119
|
+
className,
|
|
1120
|
+
objectDef,
|
|
1121
|
+
options,
|
|
1122
|
+
routeDir
|
|
1123
|
+
);
|
|
944
1124
|
const serializers = resolveStandardRouteSerializers(
|
|
945
1125
|
objectDef.decoratorConfig?.api
|
|
946
1126
|
);
|
|
@@ -951,7 +1131,8 @@ function generateCollectionRouteTemplate(_projectRoot, className, objectDef, inc
|
|
|
951
1131
|
import { error, json } from '@sveltejs/kit';
|
|
952
1132
|
${serializerImports ? `${serializerImports}
|
|
953
1133
|
` : ""}import { getCollection } from '$lib/server/smrt';
|
|
954
|
-
|
|
1134
|
+
${modelType.importStatement ? `${modelType.importStatement}
|
|
1135
|
+
` : ""}import type { RequestHandler } from './$types';
|
|
955
1136
|
// Note: ${className} is auto-registered by the Vite plugin scanner
|
|
956
1137
|
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPost ? generateWritablePolicyHelper(objectDef) : ""}`;
|
|
957
1138
|
const getHandler = hasGet ? `
|
|
@@ -961,7 +1142,7 @@ ${routeGuardPreamble(objectDef, false)}
|
|
|
961
1142
|
const limit = Number(url.searchParams.get('limit')) || 50;
|
|
962
1143
|
const offset = Number(url.searchParams.get('offset')) || 0;
|
|
963
1144
|
|
|
964
|
-
${generateCollectionLoad(className)}
|
|
1145
|
+
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
965
1146
|
const items = await collection.list({ limit, offset });
|
|
966
1147
|
const count = await collection.count();
|
|
967
1148
|
${serializers.listItemSerializerName ? `
|
|
@@ -978,9 +1159,10 @@ ${serializers.listItemSerializerName ? `
|
|
|
978
1159
|
// Create new ${className.toLowerCase()}
|
|
979
1160
|
export const POST: RequestHandler = async ({ locals, request }) => {
|
|
980
1161
|
${routeGuardPreamble(objectDef, true)}
|
|
981
|
-
const
|
|
1162
|
+
const body: unknown = await request.json();
|
|
1163
|
+
const data = applyWritablePolicy(body);
|
|
982
1164
|
|
|
983
|
-
${generateCollectionLoad(className)}
|
|
1165
|
+
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
984
1166
|
const item = await collection.create(data);
|
|
985
1167
|
await item.save();
|
|
986
1168
|
${serializers.itemSerializerName ? `
|
|
@@ -993,7 +1175,7 @@ ${serializers.itemSerializerName ? `
|
|
|
993
1175
|
return imports + getHandler + postHandler;
|
|
994
1176
|
}
|
|
995
1177
|
function generateCollectionLoad(className, options = {}) {
|
|
996
|
-
const genericSuffix = options.
|
|
1178
|
+
const genericSuffix = options.typeName ? `<${options.typeName}>` : "";
|
|
997
1179
|
return [
|
|
998
1180
|
` const collection = await getCollection${genericSuffix}(`,
|
|
999
1181
|
` '${className}',`,
|
|
@@ -1031,11 +1213,18 @@ function generateClassNotRegisteredError(className) {
|
|
|
1031
1213
|
" );"
|
|
1032
1214
|
].join("\n");
|
|
1033
1215
|
}
|
|
1034
|
-
function generateItemRouteTemplate(
|
|
1216
|
+
function generateItemRouteTemplate(projectRoot, className, objectDef, includedActions, options, routeDir) {
|
|
1035
1217
|
const hasGet = includedActions.includes("get");
|
|
1036
1218
|
const hasPut = includedActions.includes("update");
|
|
1037
1219
|
const hasDelete = includedActions.includes("delete");
|
|
1038
1220
|
const simpleClassName = extractSimpleClassName(className);
|
|
1221
|
+
const modelType = resolveObjectTypeReference(
|
|
1222
|
+
projectRoot,
|
|
1223
|
+
className,
|
|
1224
|
+
objectDef,
|
|
1225
|
+
options,
|
|
1226
|
+
routeDir
|
|
1227
|
+
);
|
|
1039
1228
|
const serializers = resolveStandardRouteSerializers(
|
|
1040
1229
|
objectDef.decoratorConfig?.api
|
|
1041
1230
|
);
|
|
@@ -1046,13 +1235,14 @@ function generateItemRouteTemplate(_projectRoot, className, objectDef, includedA
|
|
|
1046
1235
|
import { error, json } from '@sveltejs/kit';
|
|
1047
1236
|
${serializerImports ? `${serializerImports}
|
|
1048
1237
|
` : ""}import { getCollection } from '$lib/server/smrt';
|
|
1049
|
-
|
|
1238
|
+
${modelType.importStatement ? `${modelType.importStatement}
|
|
1239
|
+
` : ""}import type { RequestHandler } from './$types';
|
|
1050
1240
|
${generateAuthGuardHelper(objectDef)}${isTenantScoped(objectDef) ? generateTenantContextHelper() : ""}${hasPut ? generateWritablePolicyHelper(objectDef) : ""}`;
|
|
1051
1241
|
const getHandler = hasGet ? `
|
|
1052
1242
|
// Get single ${simpleClassName.toLowerCase()}
|
|
1053
1243
|
export const GET: RequestHandler = async ({ locals, params }) => {
|
|
1054
1244
|
${routeGuardPreamble(objectDef, false)}
|
|
1055
|
-
${generateCollectionLoad(className, {
|
|
1245
|
+
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1056
1246
|
const item = await collection.get(params.id);
|
|
1057
1247
|
${generateNotFoundError(className)}
|
|
1058
1248
|
${serializers.itemSerializerName ? `
|
|
@@ -1066,11 +1256,12 @@ ${serializers.itemSerializerName ? `
|
|
|
1066
1256
|
// Update ${simpleClassName.toLowerCase()}
|
|
1067
1257
|
export const PUT: RequestHandler = async ({ locals, params, request }) => {
|
|
1068
1258
|
${routeGuardPreamble(objectDef, true)}
|
|
1069
|
-
${generateCollectionLoad(className, {
|
|
1259
|
+
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1070
1260
|
const item = await collection.get(params.id);
|
|
1071
1261
|
${generateNotFoundError(className)}
|
|
1072
1262
|
|
|
1073
|
-
const
|
|
1263
|
+
const body: unknown = await request.json();
|
|
1264
|
+
const data = applyWritablePolicy(body);
|
|
1074
1265
|
Object.assign(item, data);
|
|
1075
1266
|
await item.save();
|
|
1076
1267
|
${serializers.itemSerializerName ? `
|
|
@@ -1084,7 +1275,7 @@ ${serializers.itemSerializerName ? `
|
|
|
1084
1275
|
// Delete ${simpleClassName.toLowerCase()}
|
|
1085
1276
|
export const DELETE: RequestHandler = async ({ locals, params }) => {
|
|
1086
1277
|
${routeGuardPreamble(objectDef, true)}
|
|
1087
|
-
${generateCollectionLoad(className, {
|
|
1278
|
+
${generateCollectionLoad(className, { typeName: modelType.typeName })}
|
|
1088
1279
|
const item = await collection.get(params.id);
|
|
1089
1280
|
${generateNotFoundError(className)}
|
|
1090
1281
|
|
|
@@ -1094,12 +1285,30 @@ ${generateNotFoundError(className)}
|
|
|
1094
1285
|
` : "";
|
|
1095
1286
|
return imports + getHandler + putHandler + deleteHandler;
|
|
1096
1287
|
}
|
|
1097
|
-
function generateActionRouteTemplate(
|
|
1288
|
+
function generateActionRouteTemplate(projectRoot, routeDir, routeSpecs, objectDef, options) {
|
|
1098
1289
|
if (routeSpecs.length === 0) {
|
|
1099
1290
|
throw new Error("Cannot generate a custom action route without handlers");
|
|
1100
1291
|
}
|
|
1101
1292
|
const [firstSpec] = routeSpecs;
|
|
1102
1293
|
const { lookupClassName, routeConfig, hostType } = firstSpec;
|
|
1294
|
+
const lookupModelType = resolveObjectTypeReference(
|
|
1295
|
+
projectRoot,
|
|
1296
|
+
lookupClassName,
|
|
1297
|
+
firstSpec.lookupObjectDef,
|
|
1298
|
+
options,
|
|
1299
|
+
routeDir
|
|
1300
|
+
);
|
|
1301
|
+
const hostModelType = resolveObjectTypeReference(
|
|
1302
|
+
projectRoot,
|
|
1303
|
+
firstSpec.hostClassName,
|
|
1304
|
+
objectDef,
|
|
1305
|
+
options,
|
|
1306
|
+
routeDir
|
|
1307
|
+
);
|
|
1308
|
+
const typeImports = mergeImportStatements([
|
|
1309
|
+
lookupModelType.importStatement,
|
|
1310
|
+
hostModelType.importStatement
|
|
1311
|
+
]);
|
|
1103
1312
|
const hasMixedHosts = routeSpecs.some(
|
|
1104
1313
|
(spec) => spec.hostType !== hostType || spec.lookupClassName !== lookupClassName || spec.routeConfig.scope !== routeConfig.scope
|
|
1105
1314
|
);
|
|
@@ -1108,14 +1317,15 @@ function generateActionRouteTemplate(_projectRoot, routeSpecs, objectDef, _optio
|
|
|
1108
1317
|
`Cannot generate mixed custom route handlers for ${lookupClassName}. All handlers sharing a route path must target the same host type and scope.`
|
|
1109
1318
|
);
|
|
1110
1319
|
}
|
|
1111
|
-
const importBlock =
|
|
1112
|
-
import {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
import type { RequestHandler } from './$types'
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1320
|
+
const importBlock = [
|
|
1321
|
+
"import { error, json } from '@sveltejs/kit';",
|
|
1322
|
+
hostType === "collection" || routeConfig.scope !== "collection" ? "import { getCollection } from '$lib/server/smrt';" : "import { ObjectRegistry } from '@happyvertical/smrt-core';",
|
|
1323
|
+
typeImports,
|
|
1324
|
+
"import type { RequestHandler } from './$types';"
|
|
1325
|
+
].filter(Boolean).join("\n");
|
|
1326
|
+
const tenantScoped = isTenantScoped(objectDef) || routeSpecs.some(
|
|
1327
|
+
(spec) => !!spec.lookupObjectDef && isTenantScoped(spec.lookupObjectDef)
|
|
1328
|
+
);
|
|
1119
1329
|
const handlers = routeSpecs.map(
|
|
1120
1330
|
(spec) => generateActionRouteHandler(
|
|
1121
1331
|
spec.lookupClassName,
|
|
@@ -1123,7 +1333,9 @@ import type { RequestHandler } from './$types';`;
|
|
|
1123
1333
|
spec.actionDef,
|
|
1124
1334
|
spec.routeConfig,
|
|
1125
1335
|
spec.hostType,
|
|
1126
|
-
tenantScoped
|
|
1336
|
+
tenantScoped,
|
|
1337
|
+
lookupModelType.typeName,
|
|
1338
|
+
hostModelType.typeName
|
|
1127
1339
|
)
|
|
1128
1340
|
).join("\n");
|
|
1129
1341
|
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
@@ -1133,7 +1345,7 @@ ${importBlock}
|
|
|
1133
1345
|
${generateAuthGuardHelper(objectDef)}${tenantScoped ? generateTenantContextHelper() : ""}
|
|
1134
1346
|
${handlers}`;
|
|
1135
1347
|
}
|
|
1136
|
-
function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped) {
|
|
1348
|
+
function generateActionRouteHandler(lookupClassName, actionName, actionDef, routeConfig, hostType, tenantScoped, lookupTypeName, hostTypeName) {
|
|
1137
1349
|
const handlerName = routeConfig.method;
|
|
1138
1350
|
const guardLines = [
|
|
1139
1351
|
` requireRouteAuth(locals, ${routeConfig.method !== "GET"});`
|
|
@@ -1145,45 +1357,32 @@ function generateActionRouteHandler(lookupClassName, actionName, actionDef, rout
|
|
|
1145
1357
|
const hasInput = actionDef.parameters.length > 0;
|
|
1146
1358
|
const needsRequest = hasInput;
|
|
1147
1359
|
const invocationArgs = buildActionInvocationArgs(actionDef);
|
|
1148
|
-
const hasPathParams = routeConfig.pathParamNames.length > 0;
|
|
1149
|
-
const pathParamsObjectLiteral = buildPathParamsObjectLiteral(
|
|
1150
|
-
routeConfig.pathParamNames
|
|
1151
|
-
);
|
|
1152
|
-
const optionsLoad = hasInput && routeConfig.method === "GET" ? hasPathParams ? [
|
|
1153
|
-
` const pathParams = ${pathParamsObjectLiteral};`,
|
|
1154
|
-
" const options = {",
|
|
1155
|
-
" ...Object.fromEntries(new URL(request.url).searchParams.entries()),",
|
|
1156
|
-
" ...pathParams,",
|
|
1157
|
-
" };",
|
|
1158
|
-
""
|
|
1159
|
-
].join("\n") : [
|
|
1160
|
-
" const options = Object.fromEntries(",
|
|
1161
|
-
" new URL(request.url).searchParams.entries(),",
|
|
1162
|
-
" );",
|
|
1163
|
-
""
|
|
1164
|
-
].join("\n") : hasInput ? hasPathParams ? [
|
|
1165
|
-
` const pathParams = ${pathParamsObjectLiteral};`,
|
|
1166
|
-
" const options = {",
|
|
1167
|
-
" ...(await request.json()),",
|
|
1168
|
-
" ...pathParams,",
|
|
1169
|
-
" };",
|
|
1170
|
-
""
|
|
1171
|
-
].join("\n") : " const options = await request.json();\n" : "";
|
|
1172
1360
|
const collectionHandlerArgs = buildRouteHandlerArgs(
|
|
1173
|
-
|
|
1361
|
+
routeConfig.pathParamNames.length > 0,
|
|
1174
1362
|
needsRequest
|
|
1175
1363
|
);
|
|
1176
1364
|
const itemHandlerArgs = buildRouteHandlerArgs(true, needsRequest);
|
|
1177
1365
|
if (hostType === "collection") {
|
|
1366
|
+
const optionsLoad2 = buildActionOptionsLoad(
|
|
1367
|
+
actionName,
|
|
1368
|
+
actionDef,
|
|
1369
|
+
routeConfig,
|
|
1370
|
+
hostTypeName,
|
|
1371
|
+
actionDef.isStatic
|
|
1372
|
+
);
|
|
1373
|
+
const receiverExpression = actionDef.isStatic ? "CollectionClass" : "typedCollection";
|
|
1374
|
+
const staticTargetLoad = actionDef.isStatic ? ` const CollectionClass = typedCollection.constructor as typeof ${hostTypeName};
|
|
1375
|
+
` : "";
|
|
1178
1376
|
return `// Custom collection method: ${actionName}
|
|
1179
1377
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1180
1378
|
${authGuardLine}
|
|
1181
|
-
${generateCollectionLoad(lookupClassName, {
|
|
1182
|
-
const typedCollection = collection as
|
|
1379
|
+
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1380
|
+
const typedCollection = collection as unknown as ${hostTypeName};
|
|
1183
1381
|
${generateCollectionNotRegisteredError(lookupClassName)}
|
|
1382
|
+
${staticTargetLoad}
|
|
1184
1383
|
|
|
1185
|
-
${
|
|
1186
|
-
|
|
1384
|
+
${optionsLoad2} const result = ${buildActionInvocationExpression(
|
|
1385
|
+
receiverExpression,
|
|
1187
1386
|
actionName,
|
|
1188
1387
|
invocationArgs
|
|
1189
1388
|
)};
|
|
@@ -1193,13 +1392,20 @@ ${optionsLoad} const result = ${buildActionInvocationExpression(
|
|
|
1193
1392
|
`;
|
|
1194
1393
|
}
|
|
1195
1394
|
if (routeConfig.scope === "collection") {
|
|
1395
|
+
const optionsLoad2 = buildActionOptionsLoad(
|
|
1396
|
+
actionName,
|
|
1397
|
+
actionDef,
|
|
1398
|
+
routeConfig,
|
|
1399
|
+
hostTypeName,
|
|
1400
|
+
true
|
|
1401
|
+
);
|
|
1196
1402
|
return `// Custom collection action: ${actionName}
|
|
1197
1403
|
export const ${handlerName}: RequestHandler = async (${collectionHandlerArgs}) => {
|
|
1198
1404
|
${authGuardLine}
|
|
1199
1405
|
const registered = ObjectRegistry.getClass('${lookupClassName}');
|
|
1200
1406
|
${generateClassNotRegisteredError(lookupClassName)}
|
|
1201
1407
|
|
|
1202
|
-
${
|
|
1408
|
+
${optionsLoad2} const ClassRef = registered.constructor as typeof ${hostTypeName};
|
|
1203
1409
|
const result = ${buildActionInvocationExpression(
|
|
1204
1410
|
"ClassRef",
|
|
1205
1411
|
actionName,
|
|
@@ -1210,10 +1416,17 @@ ${optionsLoad} const ClassRef = registered.constructor as any;
|
|
|
1210
1416
|
};
|
|
1211
1417
|
`;
|
|
1212
1418
|
}
|
|
1419
|
+
const optionsLoad = buildActionOptionsLoad(
|
|
1420
|
+
actionName,
|
|
1421
|
+
actionDef,
|
|
1422
|
+
routeConfig,
|
|
1423
|
+
hostTypeName,
|
|
1424
|
+
false
|
|
1425
|
+
);
|
|
1213
1426
|
return `// Custom action: ${actionName}
|
|
1214
1427
|
export const ${handlerName}: RequestHandler = async (${itemHandlerArgs}) => {
|
|
1215
1428
|
${authGuardLine}
|
|
1216
|
-
${generateCollectionLoad(lookupClassName, {
|
|
1429
|
+
${generateCollectionLoad(lookupClassName, { typeName: lookupTypeName })}
|
|
1217
1430
|
const item = await collection.get(params.id);
|
|
1218
1431
|
${generateNotFoundError(lookupClassName)}
|
|
1219
1432
|
|
|
@@ -1243,7 +1456,7 @@ import type { RequestHandler } from './$types';
|
|
|
1243
1456
|
const INCLUDE_DOCS_BY_DEFAULT = ${JSON.stringify(includeDocs)};
|
|
1244
1457
|
const INCLUDE_PROMPTS_BY_DEFAULT = ${JSON.stringify(includePrompts)};
|
|
1245
1458
|
const REQUIRE_ADMIN = ${JSON.stringify(requireAdmin)};
|
|
1246
|
-
const KNOWLEDGE_ARTIFACT: Record<string,
|
|
1459
|
+
const KNOWLEDGE_ARTIFACT: Record<string, unknown> | null = ${artifactLiteral};
|
|
1247
1460
|
|
|
1248
1461
|
if (!dev && !REQUIRE_ADMIN) {
|
|
1249
1462
|
console.warn('[smrt] PUBLIC knowledge API route enabled; unauthenticated responses are sanitized.');
|
|
@@ -1252,7 +1465,7 @@ if (!dev && !REQUIRE_ADMIN) {
|
|
|
1252
1465
|
export const GET: RequestHandler = async ({ locals, url, setHeaders }) => {
|
|
1253
1466
|
setHeaders({ 'cache-control': 'private, no-store' });
|
|
1254
1467
|
|
|
1255
|
-
if (!dev && REQUIRE_ADMIN && !isKnowledgeAdmin(locals
|
|
1468
|
+
if (!dev && REQUIRE_ADMIN && !isKnowledgeAdmin(locals)) {
|
|
1256
1469
|
throw error(403, 'SMRT knowledge requires dev mode or admin access');
|
|
1257
1470
|
}
|
|
1258
1471
|
|
|
@@ -1271,7 +1484,7 @@ export const GET: RequestHandler = async ({ locals, url, setHeaders }) => {
|
|
|
1271
1484
|
}));
|
|
1272
1485
|
};
|
|
1273
1486
|
|
|
1274
|
-
function readKnowledgeArtifact(): Record<string,
|
|
1487
|
+
function readKnowledgeArtifact(): Record<string, unknown> {
|
|
1275
1488
|
if (!KNOWLEDGE_ARTIFACT) throw error(404, 'SMRT knowledge artifact not found');
|
|
1276
1489
|
return KNOWLEDGE_ARTIFACT;
|
|
1277
1490
|
}
|
|
@@ -1283,9 +1496,9 @@ function queryBoolean(url: URL, name: string, defaultValue: boolean): boolean {
|
|
|
1283
1496
|
}
|
|
1284
1497
|
|
|
1285
1498
|
function sanitizeKnowledgeArtifact(
|
|
1286
|
-
artifact: Record<string,
|
|
1499
|
+
artifact: Record<string, unknown>,
|
|
1287
1500
|
options: { includeDocs: boolean; includePrompts: boolean; publicAccess: boolean },
|
|
1288
|
-
): Record<string,
|
|
1501
|
+
): Record<string, unknown> {
|
|
1289
1502
|
const sanitized = { ...artifact };
|
|
1290
1503
|
|
|
1291
1504
|
if (!options.includeDocs) {
|
|
@@ -1306,21 +1519,32 @@ function sanitizeKnowledgeArtifact(
|
|
|
1306
1519
|
return sanitized;
|
|
1307
1520
|
}
|
|
1308
1521
|
|
|
1309
|
-
function
|
|
1310
|
-
if (
|
|
1522
|
+
function recordValue(value: unknown, key: string): unknown {
|
|
1523
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
1524
|
+
return (value as Record<string, unknown>)[key];
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function isKnowledgeAdmin(locals: unknown): boolean {
|
|
1528
|
+
if (!locals || typeof locals !== 'object') return false;
|
|
1529
|
+
const localRecord = locals as Record<string, unknown>;
|
|
1530
|
+
if (
|
|
1531
|
+
localRecord.smrtKnowledgeAdmin === true ||
|
|
1532
|
+
localRecord.smrtAdmin === true
|
|
1533
|
+
) {
|
|
1311
1534
|
return true;
|
|
1312
1535
|
}
|
|
1313
1536
|
|
|
1314
|
-
const userRoles = rolesFrom(
|
|
1315
|
-
const sessionRoles = rolesFrom(
|
|
1537
|
+
const userRoles = rolesFrom(localRecord.user);
|
|
1538
|
+
const sessionRoles = rolesFrom(recordValue(localRecord.session, 'user'));
|
|
1316
1539
|
return [...userRoles, ...sessionRoles].some((role) =>
|
|
1317
1540
|
['admin', 'owner', 'superadmin'].includes(role),
|
|
1318
1541
|
);
|
|
1319
1542
|
}
|
|
1320
1543
|
|
|
1321
|
-
function rolesFrom(value:
|
|
1544
|
+
function rolesFrom(value: unknown): string[] {
|
|
1322
1545
|
if (!value || typeof value !== 'object') return [];
|
|
1323
|
-
const
|
|
1546
|
+
const roleList = recordValue(value, 'roles');
|
|
1547
|
+
const roles = Array.isArray(roleList) ? roleList : [recordValue(value, 'role')];
|
|
1324
1548
|
return roles
|
|
1325
1549
|
.filter((role): role is string => typeof role === 'string')
|
|
1326
1550
|
.map((role) => role.toLowerCase());
|