bunsane 0.3.0 → 0.3.2

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.
Files changed (54) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGELOG.md +104 -0
  3. package/CLAUDE.md +20 -0
  4. package/config/cache.config.ts +35 -1
  5. package/core/App.ts +24 -1060
  6. package/core/ArcheType.ts +78 -2110
  7. package/core/Entity.ts +136 -41
  8. package/core/RequestContext.ts +85 -36
  9. package/core/RequestLoaders.ts +89 -31
  10. package/core/SchedulerManager.ts +13 -13
  11. package/core/app/bootstrap.ts +133 -0
  12. package/core/app/cors.ts +94 -0
  13. package/core/app/graphqlSetup.ts +56 -0
  14. package/core/app/healthEndpoints.ts +31 -0
  15. package/core/app/metricsCollector.ts +27 -0
  16. package/core/app/preparedStatementWarmup.ts +55 -0
  17. package/core/app/processHandlers.ts +43 -0
  18. package/core/app/requestRouter.ts +309 -0
  19. package/core/app/restRegistry.ts +72 -0
  20. package/core/app/shutdown.ts +97 -0
  21. package/core/app/studioRouter.ts +83 -0
  22. package/core/archetype/customTypes.ts +100 -0
  23. package/core/archetype/decorators.ts +171 -0
  24. package/core/archetype/fieldResolvers.ts +621 -0
  25. package/core/archetype/helpers.ts +29 -0
  26. package/core/archetype/relationLoader.ts +118 -0
  27. package/core/archetype/schemaBuilder.ts +141 -0
  28. package/core/archetype/weaver.ts +218 -0
  29. package/core/archetype/zodSchemaBuilder.ts +527 -0
  30. package/core/cache/CacheManager.ts +144 -9
  31. package/core/components/BaseComponent.ts +12 -2
  32. package/core/middleware/AccessLog.ts +8 -1
  33. package/database/PreparedStatementCache.ts +17 -16
  34. package/database/cancellable.ts +22 -0
  35. package/database/instrumentedDb.ts +141 -0
  36. package/docs/RFC_APP_REFACTOR.md +248 -0
  37. package/docs/RFC_REFACTOR_TARGETS.md +251 -0
  38. package/package.json +1 -1
  39. package/query/ComponentInclusionNode.ts +5 -5
  40. package/query/Query.ts +65 -48
  41. package/service/ServiceRegistry.ts +7 -1
  42. package/service/index.ts +4 -2
  43. package/tests/integration/loaders/RequestLoaders.abort.test.ts +82 -0
  44. package/tests/integration/query/Query.abort.test.ts +66 -0
  45. package/tests/unit/cache/CacheManager.test.ts +152 -1
  46. package/tests/unit/database/cancellable.test.ts +81 -0
  47. package/tests/unit/database/instrumentedDb.test.ts +160 -0
  48. package/tests/unit/entity/Entity.components.test.ts +73 -0
  49. package/tests/unit/entity/Entity.drainSideEffects.test.ts +51 -0
  50. package/tests/unit/entity/Entity.reload.test.ts +63 -0
  51. package/tests/unit/entity/Entity.requireComponents.test.ts +72 -0
  52. package/tests/unit/query/Query.emptyString.test.ts +69 -0
  53. package/tests/unit/query/Query.test.ts +6 -4
  54. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +95 -0
@@ -0,0 +1,118 @@
1
+ import { Entity } from "../Entity";
2
+ import { getMetadataStorage } from "../metadata";
3
+ import { Query } from "../../query";
4
+
5
+ /**
6
+ * Populate relation fields on an entity according to the archetype's relationMap.
7
+ * Extracted from BaseArcheType.populateRelations().
8
+ */
9
+ export async function populateRelations(archetype: any, entity: Entity): Promise<void> {
10
+ const storage = getMetadataStorage();
11
+
12
+ for (const [fieldName, relatedArchetype] of Object.entries(archetype.relationMap)) {
13
+ const relationType = archetype.relationTypes[fieldName];
14
+ const relationOptions = archetype.relationOptions[fieldName];
15
+
16
+ if (relationType === "belongsTo") {
17
+ const foreignKey = relationOptions?.foreignKey;
18
+ if (foreignKey) {
19
+ let foreignId: string | undefined;
20
+
21
+ if (foreignKey.includes('.')) {
22
+ const [innerField, propName] = foreignKey.split('.');
23
+ const compCtor = archetype.componentMap[innerField!];
24
+ if (compCtor) {
25
+ const componentInstance = await entity.get(compCtor as any);
26
+ if (componentInstance && (componentInstance as any)[propName!] !== undefined) {
27
+ foreignId = (componentInstance as any)[propName!];
28
+ }
29
+ }
30
+ } else {
31
+ const candidateComponents: Array<{ compCtor: any }> = [];
32
+ for (const compCtor of Object.values(archetype.componentMap)) {
33
+ const compCtorAny = compCtor as any;
34
+ const typeId = storage.getComponentId(compCtorAny.name);
35
+ const componentProps = storage.getComponentProperties(typeId);
36
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
37
+ if (hasForeignKey) {
38
+ candidateComponents.push({ compCtor: compCtorAny });
39
+ }
40
+ }
41
+
42
+ if (candidateComponents.length > 0) {
43
+ const componentInstances = await Promise.all(
44
+ candidateComponents.map(({ compCtor }) => entity.get(compCtor as any))
45
+ );
46
+
47
+ for (const componentInstance of componentInstances) {
48
+ if (componentInstance && (componentInstance as any)[foreignKey] !== undefined) {
49
+ foreignId = (componentInstance as any)[foreignKey];
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ if (!foreignId && foreignKey === 'id') {
57
+ foreignId = entity.id;
58
+ }
59
+
60
+ if (foreignId) {
61
+ let relatedArchetypeInstance: any;
62
+ if (typeof relatedArchetype === "function") {
63
+ relatedArchetypeInstance = new (relatedArchetype as any)();
64
+ } else {
65
+ const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
66
+ if (relatedArchetypeMetadata) {
67
+ relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
68
+ } else {
69
+ continue;
70
+ }
71
+ }
72
+
73
+ const relatedEntity = await relatedArchetypeInstance.getEntityWithID(foreignId);
74
+ if (relatedEntity) {
75
+ (entity as any)[fieldName] = relatedEntity;
76
+ }
77
+ }
78
+ }
79
+ } else if (relationType === "hasMany") {
80
+ const foreignKey = relationOptions?.foreignKey;
81
+ if (foreignKey) {
82
+ let relatedArchetypeInstance: any;
83
+ if (typeof relatedArchetype === "function") {
84
+ relatedArchetypeInstance = new (relatedArchetype as any)();
85
+ } else {
86
+ const relatedArchetypeMetadata = storage.archetypes.find((a) => a.name === relatedArchetype);
87
+ if (relatedArchetypeMetadata) {
88
+ relatedArchetypeInstance = new (relatedArchetypeMetadata.target as any)();
89
+ } else {
90
+ continue;
91
+ }
92
+ }
93
+
94
+ let foreignKeyComponent: any = null;
95
+ for (const compCtor of Object.values(relatedArchetypeInstance.componentMap)) {
96
+ const compCtorAny = compCtor as any;
97
+ const typeId = storage.getComponentId(compCtorAny.name);
98
+ const componentProps = storage.getComponentProperties(typeId);
99
+ const hasForeignKey = componentProps.some(prop => prop.propertyKey === foreignKey);
100
+ if (hasForeignKey) {
101
+ foreignKeyComponent = compCtorAny;
102
+ break;
103
+ }
104
+ }
105
+
106
+ if (foreignKeyComponent) {
107
+ const matchingEntities = await new Query()
108
+ .with(foreignKeyComponent, {
109
+ filters: [{ field: foreignKey, operator: '=', value: entity.id }]
110
+ })
111
+ .exec();
112
+
113
+ (entity as any)[fieldName] = matchingEntities;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,141 @@
1
+ import type { BaseComponent } from "../components";
2
+ import type { ArcheTypeFieldOptions } from "../metadata/definitions/ArcheType";
3
+ import { z, ZodObject } from "zod";
4
+ import { asEnumType } from "@gqloom/zod";
5
+ import { getMetadataStorage } from "../metadata";
6
+ import { compNameToFieldName, primitiveTypes } from "./helpers";
7
+ import { customTypeRegistry } from "./customTypes";
8
+
9
+ // Component-level schema cache
10
+ export const componentSchemaCache = new Map<string, ZodObject<any>>();
11
+
12
+ // Enum schema cache to prevent duplicate registrations
13
+ export const enumSchemaCache = new Map<string, any>();
14
+
15
+ /**
16
+ * Generate Zod schema for a component and cache it.
17
+ */
18
+ export function getOrCreateComponentSchema(
19
+ componentCtor: new (...args: any[]) => BaseComponent,
20
+ componentId: string,
21
+ fieldOptions?: ArcheTypeFieldOptions
22
+ ): any | null {
23
+ if (componentSchemaCache.has(componentId)) {
24
+ return componentSchemaCache.get(componentId)!;
25
+ }
26
+
27
+ const storage = getMetadataStorage();
28
+ const props = storage.getComponentProperties(componentId);
29
+
30
+ if (props.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ const zodFields: Record<string, any> = {
35
+ __typename: z
36
+ .literal(compNameToFieldName(componentCtor.name))
37
+ .nullish(),
38
+ };
39
+
40
+ for (const prop of props) {
41
+ if (prop.isPrimitive) {
42
+ switch (prop.propertyType) {
43
+ case String:
44
+ zodFields[prop.propertyKey] = z.string();
45
+ break;
46
+ case Number:
47
+ zodFields[prop.propertyKey] = z.number();
48
+ break;
49
+ case Boolean:
50
+ zodFields[prop.propertyKey] = z.boolean();
51
+ break;
52
+ case Date:
53
+ zodFields[prop.propertyKey] = z.date();
54
+ break;
55
+ default:
56
+ console.warn(`[ArcheType] Unknown primitive type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
57
+ zodFields[prop.propertyKey] = z.string();
58
+ }
59
+ if (prop.isOptional) {
60
+ zodFields[prop.propertyKey] =
61
+ zodFields[prop.propertyKey].optional();
62
+ }
63
+ } else if (prop.isEnum && prop.enumValues && prop.enumKeys) {
64
+ const enumTypeName =
65
+ prop.propertyType?.name ||
66
+ `${componentCtor.name}_${prop.propertyKey}_Enum`;
67
+
68
+ let enumSchema = enumSchemaCache.get(enumTypeName);
69
+
70
+ if (!enumSchema) {
71
+ enumSchema = z
72
+ .enum(prop.enumValues as any)
73
+ .register(asEnumType, {
74
+ name: enumTypeName,
75
+ valuesConfig: prop.enumKeys.reduce(
76
+ (
77
+ acc: Record<string, { description: string }>,
78
+ key,
79
+ idx
80
+ ) => {
81
+ acc[key] = { description: prop.enumValues![idx]! };
82
+ return acc;
83
+ },
84
+ {}
85
+ ),
86
+ });
87
+ enumSchemaCache.set(enumTypeName, enumSchema);
88
+ }
89
+
90
+ zodFields[prop.propertyKey] = enumSchema;
91
+ if (prop.isOptional) {
92
+ zodFields[prop.propertyKey] =
93
+ zodFields[prop.propertyKey].optional();
94
+ }
95
+ } else if (customTypeRegistry.has(prop.propertyType)) {
96
+ zodFields[prop.propertyKey] = customTypeRegistry.get(
97
+ prop.propertyType
98
+ )!;
99
+ if (prop.isOptional) {
100
+ zodFields[prop.propertyKey] =
101
+ zodFields[prop.propertyKey].optional();
102
+ }
103
+ } else if (prop.arrayOf) {
104
+ if (customTypeRegistry.has(prop.arrayOf)) {
105
+ zodFields[prop.propertyKey] = z.array(customTypeRegistry.get(prop.arrayOf)!);
106
+ } else if (primitiveTypes.includes(prop.arrayOf)) {
107
+ if (prop.arrayOf === String) {
108
+ zodFields[prop.propertyKey] = z.array(z.string());
109
+ } else if (prop.arrayOf === Number) {
110
+ zodFields[prop.propertyKey] = z.array(z.number());
111
+ } else if (prop.arrayOf === Boolean) {
112
+ zodFields[prop.propertyKey] = z.array(z.boolean());
113
+ } else if (prop.arrayOf === Date) {
114
+ zodFields[prop.propertyKey] = z.array(z.date());
115
+ }
116
+ } else {
117
+ console.warn(`[ArcheType] Unknown array element type for ${componentCtor.name}.${prop.propertyKey}: ${prop.arrayOf?.name}. Falling back to z.array(z.string())`);
118
+ zodFields[prop.propertyKey] = z.array(z.string());
119
+ }
120
+ if (prop.isOptional) {
121
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].optional();
122
+ }
123
+ } else {
124
+ console.warn(`[ArcheType] Unknown type for ${componentCtor.name}.${prop.propertyKey}: ${prop.propertyType?.name}. Falling back to z.string()`);
125
+ zodFields[prop.propertyKey] = z.string();
126
+ if (prop.isOptional) {
127
+ zodFields[prop.propertyKey] =
128
+ zodFields[prop.propertyKey].optional();
129
+ }
130
+ }
131
+
132
+ if (fieldOptions?.nullable) {
133
+ zodFields[prop.propertyKey] = zodFields[prop.propertyKey].nullish();
134
+ }
135
+ }
136
+
137
+ const componentSchema = z.object(zodFields);
138
+ componentSchemaCache.set(componentId, componentSchema);
139
+
140
+ return componentSchema;
141
+ }
@@ -0,0 +1,218 @@
1
+ import { ZodObject } from "zod";
2
+ import { weave } from "@gqloom/core";
3
+ import { ZodWeaver } from "@gqloom/zod";
4
+ import { printSchema } from "graphql";
5
+ import { getMetadataStorage } from "../metadata";
6
+ import { componentSchemaCache } from "./schemaBuilder";
7
+ import { inputTypeRegistry, customTypeNameRegistry } from "./customTypes";
8
+
9
+ export const archetypeSchemaCache = new Map<
10
+ string,
11
+ { zodSchema: ZodObject<any>; graphqlSchema: string }
12
+ >();
13
+ export const allArchetypeZodObjects = new Map<string, ZodObject<any>>();
14
+
15
+ export function getArchetypeSchema(archetypeName: string, excludeRelations = false, excludeFunctions = false) {
16
+ const cacheKey = `${archetypeName}_${excludeRelations}_${excludeFunctions}`;
17
+ return archetypeSchemaCache.get(cacheKey);
18
+ }
19
+
20
+ export function getAllArchetypeSchemas() {
21
+ return Array.from(archetypeSchemaCache.entries())
22
+ .filter(([key]) => key.endsWith('_false_false'))
23
+ .map(([, value]) => value);
24
+ }
25
+
26
+ export function weaveAllArchetypes() {
27
+ const storage = getMetadataStorage();
28
+ const archetypeNames: string[] = [];
29
+
30
+ for (const archetypeMetadata of storage.archetypes) {
31
+ const archetypeName = archetypeMetadata.name;
32
+ archetypeNames.push(archetypeName);
33
+ const fullSchemaCacheKey = `${archetypeName}_false_false`;
34
+ if (!archetypeSchemaCache.has(fullSchemaCacheKey)) {
35
+ try {
36
+ const ArchetypeClass = archetypeMetadata.target as any;
37
+ const instance = new ArchetypeClass();
38
+ instance.getZodObjectSchema();
39
+ } catch (error) {
40
+ console.warn(
41
+ `Could not generate schema for archetype ${archetypeName}:`,
42
+ error
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ if (allArchetypeZodObjects.size === 0) {
49
+ return null;
50
+ }
51
+ const archetypeSchemas = Array.from(allArchetypeZodObjects.values());
52
+ const componentSchemas = Array.from(componentSchemaCache.values());
53
+
54
+ const allSchemas = archetypeSchemas;
55
+
56
+ try {
57
+ const schema = weave(ZodWeaver, ...allSchemas);
58
+ let schemaString = printSchema(schema);
59
+
60
+ if (!schemaString.includes('scalar Date')) {
61
+ schemaString = 'scalar Date\n\n' + schemaString;
62
+ }
63
+
64
+ schemaString = schemaString.replace(/\bid:\s*String\b/g, "id: ID");
65
+
66
+ schemaString = schemaString.replace(/\b(\w*_at|\w*_date|\w*Date|date\w*):\s*String(!?)/gi, (match, fieldName, nullable) => {
67
+ return `${fieldName}: Date${nullable}`;
68
+ });
69
+
70
+ for (const archetypeMetadata of storage.archetypes) {
71
+ const archetypeName = archetypeMetadata.name;
72
+ try {
73
+ const ArchetypeClass = archetypeMetadata.target as any;
74
+ const instance = new ArchetypeClass();
75
+
76
+ for (const [field, relatedArcheType] of Object.entries(instance.relationMap)) {
77
+ const relationType = instance.relationTypes[field];
78
+ const isArray = relationType === "hasMany" || relationType === "belongsToMany";
79
+
80
+ let relatedTypeName: string;
81
+ if (typeof relatedArcheType === "string") {
82
+ relatedTypeName = relatedArcheType;
83
+ } else {
84
+ const relatedArchetypeId = storage.getComponentId((relatedArcheType as any).name);
85
+ const relatedArchetypeMetadata = storage.archetypes.find(
86
+ (a) => a.typeId === relatedArchetypeId
87
+ );
88
+ relatedTypeName = relatedArchetypeMetadata?.name || (relatedArcheType as any).name.replace(/ArcheType$/, "");
89
+ }
90
+
91
+ if (isArray) {
92
+ const hasDescription = new RegExp(`"""Reference to ${relatedTypeName} type"""[\\s\\S]{0,50}${field}:`).test(schemaString);
93
+ if (!hasDescription) {
94
+ const addDescPattern = new RegExp(
95
+ `(type ${archetypeName} \\{[\\s\\S]*?)(\\n\\s+)(${field}:\\s*\\[String!?\\]!?)`,
96
+ "g"
97
+ );
98
+ schemaString = schemaString.replace(
99
+ addDescPattern,
100
+ `$1$2"""Reference to ${relatedTypeName} type"""$2$3`
101
+ );
102
+ }
103
+
104
+ const shouldBeRequired = instance.relationOptions[field]?.nullable === false;
105
+ const suffix = shouldBeRequired ? "!" : "";
106
+ const replacePattern = new RegExp(
107
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)\\[String!?\\](!?)`,
108
+ "g"
109
+ );
110
+ schemaString = schemaString.replace(
111
+ replacePattern,
112
+ `$1[${relatedTypeName}!]${suffix}`
113
+ );
114
+ } else {
115
+ const pattern = new RegExp(
116
+ `(type ${archetypeName} \\{[\\s\\S]*?${field}:\\s*)String(!?)`,
117
+ "g"
118
+ );
119
+ const isNullable = instance.relationOptions[field]?.nullable;
120
+ const suffix = isNullable ? "" : "!";
121
+ schemaString = schemaString.replace(
122
+ pattern,
123
+ `$1${relatedTypeName}${suffix}`
124
+ );
125
+ }
126
+ }
127
+ } catch (error) {
128
+ console.warn(`Could not process relations for archetype ${archetypeMetadata.name}:`, error);
129
+ }
130
+
131
+ if (archetypeMetadata.functions) {
132
+ for (const { propertyKey, options } of archetypeMetadata.functions) {
133
+
134
+ if (options?.args && options.args.length > 0) {
135
+ const argDefs: string[] = [];
136
+ for (const arg of options.args) {
137
+ let argTypeName: string;
138
+
139
+ const inputTypeName = inputTypeRegistry.get(arg.type);
140
+ if (inputTypeName) {
141
+ argTypeName = inputTypeName;
142
+ } else {
143
+ const registeredTypeName = customTypeNameRegistry.get(arg.type);
144
+ if (registeredTypeName) {
145
+ argTypeName = registeredTypeName;
146
+ } else if (arg.type === String) {
147
+ argTypeName = 'String';
148
+ } else if (arg.type === Number) {
149
+ argTypeName = 'Float';
150
+ } else if (arg.type === Boolean) {
151
+ argTypeName = 'Boolean';
152
+ } else if (arg.type === Date) {
153
+ argTypeName = 'Date';
154
+ } else if (arg.type?.name) {
155
+ argTypeName = arg.type.name;
156
+ } else {
157
+ argTypeName = 'String';
158
+ }
159
+ }
160
+
161
+ const nullable = arg.nullable ? '' : '!';
162
+ argDefs.push(`${arg.name}: ${argTypeName}${nullable}`);
163
+ }
164
+
165
+ const argsString = argDefs.join(', ');
166
+ const escapedKey = propertyKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
167
+
168
+ const argPattern = new RegExp(
169
+ `(\\s+)(${escapedKey}\\??\\s*:\\s*)([^\\n]+)`,
170
+ 'g'
171
+ );
172
+
173
+ schemaString = schemaString.replace(
174
+ argPattern,
175
+ (match, leadingSpace, fieldDef, returnType) => {
176
+ return `${leadingSpace}${fieldDef.trim().replace(':', '')}(${argsString}): ${returnType.trim()}`;
177
+ }
178
+ );
179
+ }
180
+
181
+ if (options?.returnType && !['string', 'number', 'boolean'].includes(options.returnType)) {
182
+ const typePattern = new RegExp(`type ${archetypeName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'g');
183
+ const typeMatch = typePattern.exec(schemaString);
184
+
185
+ if (typeMatch) {
186
+ const typeBody = typeMatch[1]!;
187
+
188
+ const fieldIndex = typeBody.indexOf(` ${propertyKey}`);
189
+ if (fieldIndex !== -1) {
190
+ const lineStart = fieldIndex;
191
+ const lineEnd = typeBody.indexOf('\n', fieldIndex);
192
+ const fieldLine = typeBody.substring(lineStart, lineEnd !== -1 ? lineEnd : typeBody.length);
193
+
194
+ const updatedLine = fieldLine.replace(/:\s*String(\??)(\s*)$/, `: ${options.returnType}$1$2`);
195
+
196
+ if (updatedLine !== fieldLine) {
197
+ const fullFieldIndex = schemaString.indexOf(typeMatch[0]) + typeMatch[0].indexOf(fieldLine);
198
+ schemaString = schemaString.substring(0, fullFieldIndex) +
199
+ updatedLine +
200
+ schemaString.substring(fullFieldIndex + fieldLine.length);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return schemaString;
210
+ } catch (error) {
211
+ console.warn(
212
+ `Failed to weave all archetypes due to duplicate types.\n` +
213
+ `Archetypes being processed: ${archetypeNames.join(', ')}\n` +
214
+ `Error: ${error}`
215
+ );
216
+ return null;
217
+ }
218
+ }