fraiseql 2.1.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/index.mjs ADDED
@@ -0,0 +1,1265 @@
1
+ // src/scalars.ts
2
+ var CustomScalar = class {
3
+ };
4
+ var SCALAR_NAMES = /* @__PURE__ */ new Set([
5
+ // Core
6
+ "ID",
7
+ "UUID",
8
+ "Json",
9
+ "Decimal",
10
+ "Vector",
11
+ // Date/Time
12
+ "DateTime",
13
+ "Date",
14
+ "Time",
15
+ "DateRange",
16
+ "Duration",
17
+ // Contact/Communication
18
+ "Email",
19
+ "PhoneNumber",
20
+ "URL",
21
+ "DomainName",
22
+ "Hostname",
23
+ // Location/Address
24
+ "PostalCode",
25
+ "Latitude",
26
+ "Longitude",
27
+ "Coordinates",
28
+ "Timezone",
29
+ "LocaleCode",
30
+ "LanguageCode",
31
+ "CountryCode",
32
+ // Financial
33
+ "IBAN",
34
+ "CUSIP",
35
+ "ISIN",
36
+ "SEDOL",
37
+ "LEI",
38
+ "MIC",
39
+ "CurrencyCode",
40
+ "Money",
41
+ "ExchangeCode",
42
+ "ExchangeRate",
43
+ "StockSymbol",
44
+ "Percentage",
45
+ // Identifiers
46
+ "Slug",
47
+ "SemanticVersion",
48
+ "HashSHA256",
49
+ "APIKey",
50
+ "LicensePlate",
51
+ "VIN",
52
+ "TrackingNumber",
53
+ "ContainerNumber",
54
+ // Networking
55
+ "IPAddress",
56
+ "IPv4",
57
+ "IPv6",
58
+ "MACAddress",
59
+ "CIDR",
60
+ "Port",
61
+ // Transportation
62
+ "AirportCode",
63
+ "PortCode",
64
+ "FlightNumber",
65
+ // Content
66
+ "Markdown",
67
+ "HTML",
68
+ "MimeType",
69
+ "Color",
70
+ "Image",
71
+ "File",
72
+ // Database
73
+ "LTree"
74
+ ]);
75
+ function isScalarType(typeName) {
76
+ return SCALAR_NAMES.has(typeName);
77
+ }
78
+
79
+ // src/types.ts
80
+ function typeToGraphQL(type) {
81
+ if (type === null || type === void 0) {
82
+ throw new Error("Cannot convert null or undefined type");
83
+ }
84
+ const typeStr = String(type);
85
+ if (type === String || typeStr === "String") {
86
+ return ["String", false];
87
+ }
88
+ if (type === Number || typeStr === "Number") {
89
+ return ["Float", false];
90
+ }
91
+ if (type === Boolean || typeStr === "Boolean") {
92
+ return ["Boolean", false];
93
+ }
94
+ if (typeof type === "function") {
95
+ return [type.name || "Object", false];
96
+ }
97
+ if (typeof type === "string") {
98
+ if (type.includes(" | null")) {
99
+ const baseType = type.replace(" | null", "").trim();
100
+ return [baseType, true];
101
+ }
102
+ if (type.endsWith("[]")) {
103
+ const elementType = type.slice(0, -2);
104
+ return [`[${elementType}!]`, false];
105
+ }
106
+ if (isScalarType(type)) {
107
+ return [type, false];
108
+ }
109
+ return [type, false];
110
+ }
111
+ throw new Error(`Unsupported type: ${type}`);
112
+ }
113
+ function extractFieldInfo(fields) {
114
+ const result = {};
115
+ for (const [fieldName, fieldType] of Object.entries(fields)) {
116
+ const [graphqlType, nullable] = typeToGraphQL(fieldType);
117
+ result[fieldName] = {
118
+ type: graphqlType,
119
+ nullable
120
+ };
121
+ }
122
+ return result;
123
+ }
124
+ function extractFunctionSignature(_name, params, returnType) {
125
+ const args = [];
126
+ for (const [paramName, paramType] of Object.entries(params)) {
127
+ if (paramName === "self" || paramName === "info") {
128
+ continue;
129
+ }
130
+ const [graphqlType, nullable] = typeToGraphQL(paramType);
131
+ args.push({
132
+ name: paramName,
133
+ type: graphqlType,
134
+ nullable
135
+ });
136
+ }
137
+ const [returnTypeStr, returnNullable] = typeToGraphQL(returnType);
138
+ const isList = returnTypeStr.startsWith("[") && returnTypeStr.endsWith("]");
139
+ return {
140
+ arguments: args,
141
+ returnType: {
142
+ type: returnTypeStr,
143
+ nullable: returnNullable,
144
+ isList
145
+ }
146
+ };
147
+ }
148
+
149
+ // src/registry.ts
150
+ var VALID_REST_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
151
+ function normaliseConfig(config2) {
152
+ const keyMap = {
153
+ sqlSource: "sql_source",
154
+ autoParams: "auto_params",
155
+ jsonbColumn: "jsonb_column",
156
+ cacheTtlSeconds: "cache_ttl_seconds",
157
+ invalidatesViews: "invalidates_views",
158
+ invalidatesFactTables: "invalidates_fact_tables",
159
+ relayCursorColumn: "relay_cursor_column",
160
+ relayCursorType: "relay_cursor_type",
161
+ requiresRole: "requires_role",
162
+ additionalViews: "additional_views"
163
+ };
164
+ const hasRestPath = "restPath" in config2 && config2.restPath != null;
165
+ const hasRestMethod = "restMethod" in config2 && config2.restMethod != null;
166
+ if (hasRestMethod && !hasRestPath) {
167
+ throw new Error("restMethod requires restPath to be set");
168
+ }
169
+ if (hasRestMethod) {
170
+ const method = String(config2.restMethod).toUpperCase();
171
+ if (!VALID_REST_METHODS.has(method)) {
172
+ throw new Error(
173
+ `Invalid REST method '${config2.restMethod}'. Must be one of: ${[...VALID_REST_METHODS].join(", ")}`
174
+ );
175
+ }
176
+ }
177
+ const result = {};
178
+ for (const [key, value] of Object.entries(config2)) {
179
+ if (key === "restPath" || key === "restMethod") {
180
+ continue;
181
+ } else if (key === "inject" && value !== null && typeof value === "object") {
182
+ const injected = {};
183
+ for (const [param, spec] of Object.entries(value)) {
184
+ const colonIdx = spec.indexOf(":");
185
+ if (colonIdx > 0) {
186
+ injected[param] = { source: spec.slice(0, colonIdx), claim: spec.slice(colonIdx + 1) };
187
+ }
188
+ }
189
+ result["inject_params"] = injected;
190
+ } else if (key === "deprecated" && typeof value === "string") {
191
+ result["deprecation"] = { reason: value };
192
+ } else {
193
+ result[keyMap[key] ?? key] = value;
194
+ }
195
+ }
196
+ if (hasRestPath) {
197
+ const method = hasRestMethod ? String(config2.restMethod).toUpperCase() : void 0;
198
+ result["rest"] = {
199
+ path: config2.restPath,
200
+ method
201
+ };
202
+ }
203
+ return result;
204
+ }
205
+ var SchemaRegistry = class {
206
+ static types = /* @__PURE__ */ new Map();
207
+ static queries = /* @__PURE__ */ new Map();
208
+ static mutations = /* @__PURE__ */ new Map();
209
+ static subscriptions = /* @__PURE__ */ new Map();
210
+ static enums = /* @__PURE__ */ new Map();
211
+ static interfaces = /* @__PURE__ */ new Map();
212
+ static inputTypes = /* @__PURE__ */ new Map();
213
+ static unions = /* @__PURE__ */ new Map();
214
+ static factTables = /* @__PURE__ */ new Map();
215
+ static aggregateQueries = /* @__PURE__ */ new Map();
216
+ static observers = /* @__PURE__ */ new Map();
217
+ static customScalars = /* @__PURE__ */ new Map();
218
+ /**
219
+ * Register a GraphQL type.
220
+ *
221
+ * @param name - Type name (e.g., "User")
222
+ * @param fields - List of field definitions
223
+ * @param description - Optional type description
224
+ * @param options - Additional type options
225
+ */
226
+ static registerType(name, fields, description, options) {
227
+ if (this.types.has(name)) {
228
+ throw new Error(
229
+ `Type '${name}' is already registered. Each name must be unique within a schema.`
230
+ );
231
+ }
232
+ const typeDef = { name, fields, description };
233
+ if (options?.relay) typeDef.relay = true;
234
+ if (options?.sqlSource) typeDef.sql_source = options.sqlSource;
235
+ if (options?.jsonbColumn) typeDef.jsonb_column = options.jsonbColumn;
236
+ if (options?.isError) typeDef.is_error = true;
237
+ if (options?.requiresRole) typeDef.requires_role = options.requiresRole;
238
+ if (options?.implements) typeDef.implements = options.implements;
239
+ this.types.set(name, typeDef);
240
+ }
241
+ /**
242
+ * Register a GraphQL query.
243
+ *
244
+ * @param name - Query name
245
+ * @param returnType - Return type name
246
+ * @param returnsList - Whether query returns a list
247
+ * @param nullable - Whether result can be null
248
+ * @param args - List of argument definitions
249
+ * @param description - Optional query description
250
+ * @param config - Additional configuration (sql_source, etc.)
251
+ */
252
+ static registerQuery(name, returnType, returnsList, nullable, args, description, config2) {
253
+ if (this.queries.has(name)) {
254
+ throw new Error(
255
+ `Query '${name}' is already registered. Each name must be unique within a schema.`
256
+ );
257
+ }
258
+ const cleanType = returnsList ? returnType.replace(/[[\]!]/g, "") : returnType;
259
+ if (config2?.relay) {
260
+ if (!returnsList) {
261
+ throw new Error(
262
+ `registerQuery('${name}'): relay: true requires returns_list to be true. Relay connections only apply to list queries.`
263
+ );
264
+ }
265
+ if (!config2.sqlSource) {
266
+ throw new Error(
267
+ `registerQuery('${name}'): relay: true requires sqlSource to be set. The compiler needs the view name to derive the cursor column.`
268
+ );
269
+ }
270
+ if (config2.autoParams) {
271
+ const ap = { ...config2.autoParams };
272
+ delete ap["limit"];
273
+ delete ap["offset"];
274
+ config2 = { ...config2, autoParams: ap };
275
+ }
276
+ }
277
+ const normalisedConfig = config2 ? normaliseConfig(config2) : void 0;
278
+ if (normalisedConfig?.rest) {
279
+ const rest = normalisedConfig.rest;
280
+ if (!rest.method) {
281
+ rest.method = "GET";
282
+ }
283
+ }
284
+ this.queries.set(name, {
285
+ name,
286
+ return_type: cleanType,
287
+ returns_list: returnsList,
288
+ nullable,
289
+ arguments: args,
290
+ description,
291
+ ...normalisedConfig
292
+ });
293
+ }
294
+ /**
295
+ * Register a GraphQL mutation.
296
+ *
297
+ * @param name - Mutation name
298
+ * @param returnType - Return type name
299
+ * @param returnsList - Whether mutation returns a list
300
+ * @param nullable - Whether result can be null
301
+ * @param args - List of argument definitions
302
+ * @param description - Optional mutation description
303
+ * @param config - Additional configuration (sql_source, operation, etc.)
304
+ */
305
+ static registerMutation(name, returnType, returnsList, nullable, args, description, config2) {
306
+ if (this.mutations.has(name)) {
307
+ throw new Error(
308
+ `Mutation '${name}' is already registered. Each name must be unique within a schema.`
309
+ );
310
+ }
311
+ const cleanType = returnsList ? returnType.replace(/[[\]!]/g, "") : returnType;
312
+ const normalisedConfig = config2 ? normaliseConfig(config2) : void 0;
313
+ if (normalisedConfig?.rest) {
314
+ const rest = normalisedConfig.rest;
315
+ if (!rest.method) {
316
+ rest.method = "POST";
317
+ }
318
+ }
319
+ this.mutations.set(name, {
320
+ name,
321
+ return_type: cleanType,
322
+ returns_list: returnsList,
323
+ nullable,
324
+ arguments: args,
325
+ description,
326
+ ...normalisedConfig
327
+ });
328
+ }
329
+ /**
330
+ * Register a GraphQL subscription.
331
+ *
332
+ * Subscriptions in FraiseQL are compiled projections of database events.
333
+ * They are sourced from LISTEN/NOTIFY or CDC, not resolver-based.
334
+ *
335
+ * @param name - Subscription name
336
+ * @param entityType - Entity type name being subscribed to
337
+ * @param nullable - Whether result can be null
338
+ * @param args - List of argument definitions (filters)
339
+ * @param description - Optional subscription description
340
+ * @param config - Additional configuration (topic, operation, etc.)
341
+ */
342
+ static registerSubscription(name, entityType, nullable, args, description, config2) {
343
+ if (this.subscriptions.has(name)) {
344
+ throw new Error(
345
+ `Subscription '${name}' is already registered. Each name must be unique within a schema.`
346
+ );
347
+ }
348
+ this.subscriptions.set(name, {
349
+ name,
350
+ entity_type: entityType,
351
+ nullable,
352
+ arguments: args,
353
+ description,
354
+ ...config2
355
+ });
356
+ }
357
+ /**
358
+ * Register a fact table definition.
359
+ *
360
+ * @param tableName - Fact table name
361
+ * @param measures - List of measure definitions
362
+ * @param dimensions - Dimension metadata
363
+ * @param denormalizedFilters - List of denormalized filter definitions
364
+ */
365
+ static registerFactTable(tableName, measures, dimensions, denormalizedFilters) {
366
+ this.factTables.set(tableName, {
367
+ table_name: tableName,
368
+ measures,
369
+ dimensions,
370
+ denormalized_filters: denormalizedFilters
371
+ });
372
+ }
373
+ /**
374
+ * Register an aggregate query definition.
375
+ *
376
+ * @param name - Query name
377
+ * @param factTable - Fact table name
378
+ * @param autoGroupBy - Auto-generate groupBy fields
379
+ * @param autoAggregates - Auto-generate aggregate fields
380
+ * @param description - Optional query description
381
+ */
382
+ static registerAggregateQuery(name, factTable, autoGroupBy, autoAggregates, description) {
383
+ this.aggregateQueries.set(name, {
384
+ name,
385
+ fact_table: factTable,
386
+ auto_group_by: autoGroupBy,
387
+ auto_aggregates: autoAggregates,
388
+ description
389
+ });
390
+ }
391
+ /**
392
+ * Register an observer.
393
+ *
394
+ * @param name - Observer function name
395
+ * @param entity - Entity type to observe
396
+ * @param event - Event type (INSERT, UPDATE, or DELETE)
397
+ * @param actions - List of action configurations
398
+ * @param condition - Optional condition expression
399
+ * @param retry - Retry configuration
400
+ */
401
+ static registerObserver(name, entity, event, actions, condition, retry) {
402
+ this.observers.set(name, {
403
+ name,
404
+ entity,
405
+ event: event.toUpperCase(),
406
+ actions,
407
+ condition,
408
+ retry: retry || {
409
+ max_attempts: 3,
410
+ backoff_strategy: "exponential",
411
+ initial_delay_ms: 100,
412
+ max_delay_ms: 6e4
413
+ }
414
+ });
415
+ }
416
+ /**
417
+ * Register a GraphQL enum type.
418
+ *
419
+ * @param name - Enum name (e.g., "OrderStatus")
420
+ * @param values - List of enum value definitions
421
+ * @param description - Optional enum description
422
+ */
423
+ static registerEnum(name, values, description) {
424
+ if (this.enums.has(name)) {
425
+ throw new Error(
426
+ `Enum '${name}' is already registered. Each name must be unique within a schema.`
427
+ );
428
+ }
429
+ this.enums.set(name, {
430
+ name,
431
+ values,
432
+ description
433
+ });
434
+ }
435
+ /**
436
+ * Register a GraphQL interface type.
437
+ *
438
+ * @param name - Interface name (e.g., "Node")
439
+ * @param fields - List of field definitions
440
+ * @param description - Optional interface description
441
+ */
442
+ static registerInterface(name, fields, description) {
443
+ if (this.interfaces.has(name)) {
444
+ throw new Error(
445
+ `Interface '${name}' is already registered. Each name must be unique within a schema.`
446
+ );
447
+ }
448
+ this.interfaces.set(name, {
449
+ name,
450
+ fields,
451
+ description
452
+ });
453
+ }
454
+ /**
455
+ * Register a GraphQL input type.
456
+ *
457
+ * @param name - Input type name (e.g., "CreateUserInput")
458
+ * @param fields - List of field definitions with optional defaults
459
+ * @param description - Optional input type description
460
+ */
461
+ static registerInputType(name, fields, description) {
462
+ if (this.inputTypes.has(name)) {
463
+ throw new Error(
464
+ `Input type '${name}' is already registered. Each name must be unique within a schema.`
465
+ );
466
+ }
467
+ this.inputTypes.set(name, {
468
+ name,
469
+ fields,
470
+ description
471
+ });
472
+ }
473
+ /**
474
+ * Register a GraphQL union type.
475
+ *
476
+ * @param name - Union name (e.g., "SearchResult")
477
+ * @param memberTypes - List of member type names
478
+ * @param description - Optional union description
479
+ */
480
+ static registerUnion(name, memberTypes, description) {
481
+ if (this.unions.has(name)) {
482
+ throw new Error(
483
+ `Union '${name}' is already registered. Each name must be unique within a schema.`
484
+ );
485
+ }
486
+ this.unions.set(name, {
487
+ name,
488
+ member_types: memberTypes,
489
+ description
490
+ });
491
+ }
492
+ /**
493
+ * Register a custom scalar.
494
+ *
495
+ * @param name - Scalar name (e.g., "Email")
496
+ * @param scalarClass - The CustomScalar subclass
497
+ * @param description - Optional scalar description
498
+ *
499
+ * @throws If scalar name is not unique
500
+ */
501
+ static registerScalar(name, scalarClass, description) {
502
+ if (this.customScalars.has(name)) {
503
+ throw new Error(
504
+ `Scalar '${name}' is already registered. Each name must be unique within a schema.`
505
+ );
506
+ }
507
+ this.customScalars.set(name, { class: scalarClass, description });
508
+ }
509
+ /**
510
+ * Get all registered custom scalars.
511
+ *
512
+ * @returns Map of scalar names to CustomScalar classes
513
+ */
514
+ static getCustomScalars() {
515
+ const result = /* @__PURE__ */ new Map();
516
+ for (const [name, { class: scalarClass }] of this.customScalars) {
517
+ result.set(name, scalarClass);
518
+ }
519
+ return result;
520
+ }
521
+ /**
522
+ * Get the complete schema as an object.
523
+ *
524
+ * @returns Schema object with types, queries, mutations, subscriptions, and analytics sections
525
+ */
526
+ static getSchema() {
527
+ const schema = {
528
+ types: Array.from(this.types.values()),
529
+ queries: Array.from(this.queries.values()),
530
+ mutations: Array.from(this.mutations.values()),
531
+ subscriptions: Array.from(this.subscriptions.values())
532
+ };
533
+ if (this.enums.size > 0) {
534
+ schema.enums = Array.from(this.enums.values());
535
+ }
536
+ if (this.interfaces.size > 0) {
537
+ schema.interfaces = Array.from(this.interfaces.values());
538
+ }
539
+ if (this.inputTypes.size > 0) {
540
+ schema.input_types = Array.from(this.inputTypes.values());
541
+ }
542
+ if (this.unions.size > 0) {
543
+ schema.unions = Array.from(this.unions.values());
544
+ }
545
+ if (this.factTables.size > 0) {
546
+ schema.fact_tables = Array.from(this.factTables.values());
547
+ }
548
+ if (this.aggregateQueries.size > 0) {
549
+ schema.aggregate_queries = Array.from(this.aggregateQueries.values());
550
+ }
551
+ if (this.observers.size > 0) {
552
+ schema.observers = Array.from(this.observers.values());
553
+ }
554
+ if (this.customScalars.size > 0) {
555
+ const customScalars = {};
556
+ for (const [name, { class: scalarClass, description }] of this.customScalars) {
557
+ customScalars[name] = {
558
+ name,
559
+ description: description ?? "Custom scalar",
560
+ validate: true
561
+ };
562
+ void scalarClass;
563
+ }
564
+ schema.customScalars = customScalars;
565
+ }
566
+ return schema;
567
+ }
568
+ /**
569
+ * Clear the registry (useful for testing).
570
+ */
571
+ static clear() {
572
+ this.types.clear();
573
+ this.queries.clear();
574
+ this.mutations.clear();
575
+ this.subscriptions.clear();
576
+ this.enums.clear();
577
+ this.interfaces.clear();
578
+ this.inputTypes.clear();
579
+ this.unions.clear();
580
+ this.factTables.clear();
581
+ this.aggregateQueries.clear();
582
+ this.observers.clear();
583
+ this.customScalars.clear();
584
+ }
585
+ };
586
+
587
+ // src/crud.ts
588
+ var ALL_OPS = /* @__PURE__ */ new Set(["read", "create", "update", "delete"]);
589
+ function pascalToSnake(name) {
590
+ return name.replace(/(?<!^)([A-Z])/g, "_$1").toLowerCase();
591
+ }
592
+ function pluralize(name) {
593
+ if (name.endsWith("s") && !name.endsWith("ss")) return name;
594
+ for (const suffix of ["ss", "sh", "ch", "x", "z"]) {
595
+ if (name.endsWith(suffix)) return name + "es";
596
+ }
597
+ if (/[^aeiou]y$/.test(name)) return name.slice(0, -1) + "ies";
598
+ return name + "s";
599
+ }
600
+ function parseCrudOps(crud) {
601
+ if (crud === true) return new Set(ALL_OPS);
602
+ if (Array.isArray(crud)) {
603
+ const unknown = crud.filter((op) => !ALL_OPS.has(op));
604
+ if (unknown.length > 0) {
605
+ throw new Error(
606
+ `Unknown CRUD operations: ${unknown.join(", ")}. Valid: read, create, update, delete`
607
+ );
608
+ }
609
+ return new Set(crud);
610
+ }
611
+ return /* @__PURE__ */ new Set();
612
+ }
613
+ function generateCrudOperations(typeName, fields, crud, sqlSource, cascade) {
614
+ const ops = parseCrudOps(crud);
615
+ if (ops.size === 0) return;
616
+ if (fields.length === 0) {
617
+ throw new Error(`Type '${typeName}' has no fields; cannot generate CRUD operations`);
618
+ }
619
+ const snake = pascalToSnake(typeName);
620
+ const view = sqlSource ?? `v_${snake}`;
621
+ const pkField = fields[0];
622
+ if (ops.has("read")) {
623
+ SchemaRegistry.registerQuery(
624
+ snake,
625
+ typeName,
626
+ false,
627
+ true,
628
+ [{ name: pkField.name, type: pkField.type, nullable: false }],
629
+ `Get ${typeName} by ID.`,
630
+ { sql_source: view }
631
+ );
632
+ SchemaRegistry.registerQuery(
633
+ pluralize(snake),
634
+ typeName,
635
+ true,
636
+ false,
637
+ [],
638
+ `List ${typeName} records.`,
639
+ { sql_source: view, auto_params: { where: true, order_by: true, limit: true, offset: true } }
640
+ );
641
+ }
642
+ if (ops.has("create")) {
643
+ const args = fields.map((f) => ({
644
+ name: f.name,
645
+ type: f.type,
646
+ nullable: f.nullable
647
+ }));
648
+ const config2 = {
649
+ sql_source: `fn_create_${snake}`,
650
+ operation: "INSERT"
651
+ };
652
+ if (cascade) config2.cascade = true;
653
+ SchemaRegistry.registerMutation(
654
+ `create_${snake}`,
655
+ typeName,
656
+ false,
657
+ false,
658
+ args,
659
+ `Create a new ${typeName}.`,
660
+ config2
661
+ );
662
+ }
663
+ if (ops.has("update")) {
664
+ const args = [
665
+ { name: pkField.name, type: pkField.type, nullable: false },
666
+ ...fields.slice(1).map((f) => ({ name: f.name, type: f.type, nullable: true }))
667
+ ];
668
+ const config2 = {
669
+ sql_source: `fn_update_${snake}`,
670
+ operation: "UPDATE"
671
+ };
672
+ if (cascade) config2.cascade = true;
673
+ SchemaRegistry.registerMutation(
674
+ `update_${snake}`,
675
+ typeName,
676
+ false,
677
+ true,
678
+ args,
679
+ `Update an existing ${typeName}.`,
680
+ config2
681
+ );
682
+ }
683
+ if (ops.has("delete")) {
684
+ const config2 = {
685
+ sql_source: `fn_delete_${snake}`,
686
+ operation: "DELETE"
687
+ };
688
+ if (cascade) config2.cascade = true;
689
+ SchemaRegistry.registerMutation(
690
+ `delete_${snake}`,
691
+ typeName,
692
+ false,
693
+ false,
694
+ [{ name: pkField.name, type: pkField.type, nullable: false }],
695
+ `Delete a ${typeName}.`,
696
+ config2
697
+ );
698
+ }
699
+ }
700
+
701
+ // src/decorators.ts
702
+ function field(options) {
703
+ return options;
704
+ }
705
+ function Type(_config) {
706
+ return function(constructor) {
707
+ const typeName = constructor.name;
708
+ SchemaRegistry.registerType(typeName, [], _config?.description, {
709
+ sqlSource: _config?.sqlSource
710
+ });
711
+ return constructor;
712
+ };
713
+ }
714
+ function Query(config2) {
715
+ return function(_target, propertyKey, descriptor) {
716
+ const originalMethod = descriptor.value;
717
+ const methodName = propertyKey;
718
+ SchemaRegistry.registerQuery(
719
+ methodName,
720
+ "Query",
721
+ // Placeholder - should be extracted from metadata
722
+ false,
723
+ // Placeholder
724
+ false,
725
+ // Placeholder
726
+ [],
727
+ // Placeholder
728
+ originalMethod?.toString?.().split("\n")[0] ?? void 0,
729
+ config2
730
+ );
731
+ return descriptor;
732
+ };
733
+ }
734
+ function Mutation(config2) {
735
+ return function(_target, propertyKey, descriptor) {
736
+ const originalMethod = descriptor.value;
737
+ const methodName = propertyKey;
738
+ SchemaRegistry.registerMutation(
739
+ methodName,
740
+ "Mutation",
741
+ // Placeholder - should be extracted from metadata
742
+ false,
743
+ // Placeholder
744
+ false,
745
+ // Placeholder
746
+ [],
747
+ // Placeholder
748
+ originalMethod?.toString?.().split("\n")[0] ?? void 0,
749
+ config2
750
+ );
751
+ return descriptor;
752
+ };
753
+ }
754
+ function enum_(name, values, config2) {
755
+ const enumValues = Object.keys(values).map((key) => ({
756
+ name: key
757
+ }));
758
+ SchemaRegistry.registerEnum(name, enumValues, config2?.description);
759
+ return values;
760
+ }
761
+ function interface_(name, fields, config2) {
762
+ SchemaRegistry.registerInterface(name, fields, config2?.description);
763
+ return {};
764
+ }
765
+ function union(name, memberTypes, config2) {
766
+ SchemaRegistry.registerUnion(name, memberTypes, config2?.description);
767
+ return {};
768
+ }
769
+ function input(name, fields, config2) {
770
+ SchemaRegistry.registerInputType(name, fields, config2?.description);
771
+ return {};
772
+ }
773
+ function registerTypeFields(typeName, fields, description, options) {
774
+ SchemaRegistry.registerType(typeName, fields, description, options);
775
+ if (options?.crud) {
776
+ generateCrudOperations(typeName, fields, options.crud, options.sqlSource, options.cascade);
777
+ }
778
+ }
779
+ function registerQuery(name, returnType, returnsList, nullable, args, description, config2) {
780
+ SchemaRegistry.registerQuery(name, returnType, returnsList, nullable, args, description, config2);
781
+ }
782
+ function registerMutation(name, returnType, returnsList, nullable, args, description, config2) {
783
+ SchemaRegistry.registerMutation(
784
+ name,
785
+ returnType,
786
+ returnsList,
787
+ nullable,
788
+ args,
789
+ description,
790
+ config2
791
+ );
792
+ }
793
+ function Subscription(config2) {
794
+ return function(_target, propertyKey, descriptor) {
795
+ const originalMethod = descriptor.value;
796
+ const methodName = propertyKey;
797
+ const entityType = config2?.entityType || "Subscription";
798
+ SchemaRegistry.registerSubscription(
799
+ methodName,
800
+ entityType,
801
+ false,
802
+ // Placeholder for nullable
803
+ [],
804
+ // Placeholder for arguments
805
+ originalMethod?.toString?.().split("\n")[0] ?? void 0,
806
+ config2
807
+ );
808
+ return descriptor;
809
+ };
810
+ }
811
+ function registerSubscription(name, entityType, nullable, args, description, config2) {
812
+ SchemaRegistry.registerSubscription(name, entityType, nullable, args, description, config2);
813
+ }
814
+ function Scalar(target) {
815
+ if (!isCustomScalarSubclass(target)) {
816
+ const name = target.name ?? "(unknown)";
817
+ throw new TypeError(
818
+ `@Scalar can only be applied to CustomScalar subclasses, got ${name}`
819
+ );
820
+ }
821
+ const instance = new target();
822
+ const scalarName = instance.name;
823
+ if (!scalarName || typeof scalarName !== "string") {
824
+ throw new Error(
825
+ `CustomScalar ${target.name} must have a 'name' property of type string`
826
+ );
827
+ }
828
+ SchemaRegistry.registerScalar(scalarName, target, target.toString());
829
+ return target;
830
+ }
831
+ function isCustomScalarSubclass(target) {
832
+ try {
833
+ return target.prototype instanceof CustomScalar || target === CustomScalar;
834
+ } catch {
835
+ return false;
836
+ }
837
+ }
838
+
839
+ // src/schema.ts
840
+ import * as fs from "fs";
841
+ var BUILTIN_SCALARS = /* @__PURE__ */ new Set(["String", "Int", "Float", "Boolean", "ID"]);
842
+ function validateSchemaBeforeExport(schema) {
843
+ const registeredTypeNames = /* @__PURE__ */ new Set([
844
+ ...schema.types.map((t) => t.name),
845
+ ...(schema.enums ?? []).map((e) => e.name),
846
+ ...BUILTIN_SCALARS
847
+ ]);
848
+ const errors = [];
849
+ for (const query of schema.queries) {
850
+ const ret = query.return_type;
851
+ if (ret && !registeredTypeNames.has(ret)) {
852
+ errors.push(
853
+ `Query '${query.name}' has return type '${ret}' which is not a registered type.`
854
+ );
855
+ }
856
+ }
857
+ for (const mutation of schema.mutations) {
858
+ const ret = mutation.return_type;
859
+ if (ret && !registeredTypeNames.has(ret)) {
860
+ errors.push(
861
+ `Mutation '${mutation.name}' has return type '${ret}' which is not a registered type.`
862
+ );
863
+ }
864
+ }
865
+ if (errors.length > 0) {
866
+ throw new Error(
867
+ `Schema validation failed before export. Fix the following errors:
868
+ - ${errors.join("\n - ")}`
869
+ );
870
+ }
871
+ }
872
+ var ConfigHolder = class {
873
+ static pendingConfig = null;
874
+ };
875
+ function config(configObj) {
876
+ ConfigHolder.pendingConfig = configObj;
877
+ }
878
+ function exportSchema(outputPath, options = {}) {
879
+ const { pretty = true } = options;
880
+ const schema = SchemaRegistry.getSchema();
881
+ validateSchemaBeforeExport(schema);
882
+ const content = pretty ? JSON.stringify(schema, null, 2) + "\n" : JSON.stringify(schema);
883
+ fs.writeFileSync(outputPath, content, { encoding: "utf-8" });
884
+ console.log(`\u2705 Schema exported to ${outputPath}`);
885
+ console.log(` Types: ${schema.types.length}`);
886
+ console.log(` Queries: ${schema.queries.length}`);
887
+ console.log(` Mutations: ${schema.mutations.length}`);
888
+ if (schema.fact_tables) {
889
+ console.log(` Fact Tables: ${schema.fact_tables.length}`);
890
+ }
891
+ if (schema.aggregate_queries) {
892
+ console.log(` Aggregate Queries: ${schema.aggregate_queries.length}`);
893
+ }
894
+ if (schema.observers) {
895
+ console.log(` Observers: ${schema.observers.length}`);
896
+ }
897
+ }
898
+ function getSchemaDict() {
899
+ return SchemaRegistry.getSchema();
900
+ }
901
+ function exportSchemaToString(options = {}) {
902
+ const { pretty = true } = options;
903
+ const schema = SchemaRegistry.getSchema();
904
+ return pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema);
905
+ }
906
+ function exportTypes(outputPath, options = {}) {
907
+ const { pretty = true } = options;
908
+ const fullSchema = SchemaRegistry.getSchema();
909
+ const minimalSchema = {
910
+ types: fullSchema.types || [],
911
+ enums: fullSchema.enums || [],
912
+ input_types: fullSchema.input_types || [],
913
+ interfaces: fullSchema.interfaces || []
914
+ };
915
+ const content = pretty ? JSON.stringify(minimalSchema, null, 2) + "\n" : JSON.stringify(minimalSchema);
916
+ fs.writeFileSync(outputPath, content, { encoding: "utf-8" });
917
+ console.log(`\u2705 Types exported to ${outputPath}`);
918
+ console.log(` Types: ${minimalSchema.types.length}`);
919
+ if (minimalSchema.enums.length > 0) {
920
+ console.log(` Enums: ${minimalSchema.enums.length}`);
921
+ }
922
+ if (minimalSchema.input_types.length > 0) {
923
+ console.log(` Input types: ${minimalSchema.input_types.length}`);
924
+ }
925
+ if (minimalSchema.interfaces.length > 0) {
926
+ console.log(` Interfaces: ${minimalSchema.interfaces.length}`);
927
+ }
928
+ console.log(` \u2192 Use with: fraiseql compile fraiseql.toml --types ${outputPath}`);
929
+ }
930
+
931
+ // src/validators.ts
932
+ var ScalarValidationError = class extends Error {
933
+ /**
934
+ * Creates a new ScalarValidationError.
935
+ *
936
+ * @param scalarName - Name of the scalar that failed validation
937
+ * @param context - The validation context ("serialize", "parseValue", or "parseLiteral")
938
+ * @param message - The underlying error message
939
+ */
940
+ constructor(scalarName, context, message) {
941
+ super(
942
+ `Scalar ${JSON.stringify(scalarName)} validation failed in ${context}: ${message}`
943
+ );
944
+ this.scalarName = scalarName;
945
+ this.context = context;
946
+ this.name = "ScalarValidationError";
947
+ }
948
+ };
949
+ function validateCustomScalar(scalarClass, value, context = "parseValue") {
950
+ const instance = new scalarClass();
951
+ const scalarName = instance.name;
952
+ try {
953
+ switch (context) {
954
+ case "serialize":
955
+ return instance.serialize(value);
956
+ case "parseValue":
957
+ return instance.parseValue(value);
958
+ case "parseLiteral":
959
+ return instance.parseLiteral(value);
960
+ default:
961
+ throw new Error(`Unknown validation context: ${context}`);
962
+ }
963
+ } catch (error) {
964
+ if (error instanceof ScalarValidationError) {
965
+ throw error;
966
+ }
967
+ const message = error instanceof Error ? error.message : String(error);
968
+ throw new ScalarValidationError(scalarName, context, message);
969
+ }
970
+ }
971
+ function getAllCustomScalars() {
972
+ return SchemaRegistry.getCustomScalars();
973
+ }
974
+
975
+ // src/observers.ts
976
+ var DEFAULT_RETRY_CONFIG = {
977
+ max_attempts: 3,
978
+ backoff_strategy: "exponential",
979
+ initial_delay_ms: 100,
980
+ max_delay_ms: 6e4
981
+ };
982
+ function Observer(config2) {
983
+ return function(_target, propertyKey, _descriptor) {
984
+ SchemaRegistry.registerObserver(
985
+ propertyKey,
986
+ config2.entity,
987
+ config2.event,
988
+ config2.actions,
989
+ config2.condition,
990
+ config2.retry
991
+ );
992
+ };
993
+ }
994
+ function webhook(url, options) {
995
+ if (url === void 0 && options?.url_env === void 0) {
996
+ throw new Error("Either url or url_env must be provided");
997
+ }
998
+ const action = {
999
+ type: "webhook",
1000
+ headers: { "Content-Type": "application/json", ...options?.headers ?? {} }
1001
+ };
1002
+ if (url !== void 0) {
1003
+ action.url = url;
1004
+ }
1005
+ if (options?.url_env !== void 0) {
1006
+ action.url_env = options.url_env;
1007
+ }
1008
+ if (options?.body_template !== void 0) {
1009
+ action.body_template = options.body_template;
1010
+ }
1011
+ return action;
1012
+ }
1013
+ function slack(channel, message, options) {
1014
+ const action = {
1015
+ type: "slack",
1016
+ channel,
1017
+ message,
1018
+ webhook_url_env: options?.webhook_url_env ?? "SLACK_WEBHOOK_URL"
1019
+ };
1020
+ if (options?.webhook_url !== void 0) {
1021
+ action.webhook_url = options.webhook_url;
1022
+ }
1023
+ return action;
1024
+ }
1025
+ function email(to, subject, body, options) {
1026
+ const action = {
1027
+ type: "email",
1028
+ to,
1029
+ subject,
1030
+ body
1031
+ };
1032
+ if (options?.from_email !== void 0) {
1033
+ action.from = options.from_email;
1034
+ }
1035
+ return action;
1036
+ }
1037
+
1038
+ // src/errors.ts
1039
+ var FraiseQLError = class extends Error {
1040
+ constructor(message, options) {
1041
+ super(message, options);
1042
+ this.name = "FraiseQLError";
1043
+ }
1044
+ };
1045
+ var GraphQLError = class extends FraiseQLError {
1046
+ errors;
1047
+ constructor(errors) {
1048
+ super(errors[0]?.message ?? "GraphQL error");
1049
+ this.name = "GraphQLError";
1050
+ this.errors = errors;
1051
+ }
1052
+ };
1053
+ var NetworkError = class extends FraiseQLError {
1054
+ constructor(message, options) {
1055
+ super(message, options);
1056
+ this.name = "NetworkError";
1057
+ }
1058
+ };
1059
+ var TimeoutError = class extends NetworkError {
1060
+ constructor(message = "Request timed out") {
1061
+ super(message);
1062
+ this.name = "TimeoutError";
1063
+ }
1064
+ };
1065
+ var AuthenticationError = class extends FraiseQLError {
1066
+ statusCode;
1067
+ constructor(statusCode) {
1068
+ super(`Authentication failed (HTTP ${statusCode})`);
1069
+ this.name = "AuthenticationError";
1070
+ this.statusCode = statusCode;
1071
+ }
1072
+ };
1073
+ var RateLimitError = class extends FraiseQLError {
1074
+ retryAfterMs;
1075
+ constructor(retryAfterMs) {
1076
+ super("Rate limit exceeded");
1077
+ this.name = "RateLimitError";
1078
+ this.retryAfterMs = retryAfterMs;
1079
+ }
1080
+ };
1081
+
1082
+ // src/http-retry.ts
1083
+ async function executeWithRetry(fn, config2 = {}) {
1084
+ const {
1085
+ maxAttempts = 1,
1086
+ baseDelayMs = 1e3,
1087
+ maxDelayMs = 3e4,
1088
+ jitter = true,
1089
+ retryOn = [NetworkError, TimeoutError],
1090
+ onRetry
1091
+ } = config2;
1092
+ let lastError;
1093
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1094
+ try {
1095
+ return await fn();
1096
+ } catch (error) {
1097
+ if (attempt === maxAttempts) throw error;
1098
+ const isRetryable = retryOn.some(
1099
+ (ErrorClass) => error instanceof ErrorClass
1100
+ );
1101
+ if (!isRetryable) throw error;
1102
+ lastError = error;
1103
+ onRetry?.(attempt, lastError);
1104
+ const delay = Math.min(
1105
+ baseDelayMs * Math.pow(2, attempt - 1),
1106
+ maxDelayMs
1107
+ );
1108
+ const actualDelay = jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
1109
+ await new Promise((resolve) => setTimeout(resolve, actualDelay));
1110
+ }
1111
+ }
1112
+ throw lastError;
1113
+ }
1114
+
1115
+ // src/client.ts
1116
+ var FraiseQLClient = class {
1117
+ url;
1118
+ authorization;
1119
+ timeoutMs;
1120
+ retry;
1121
+ extraHeaders;
1122
+ fetchFn;
1123
+ constructor(urlOrConfig) {
1124
+ const config2 = typeof urlOrConfig === "string" ? { url: urlOrConfig } : urlOrConfig;
1125
+ this.url = config2.url;
1126
+ this.authorization = config2.authorization;
1127
+ this.timeoutMs = config2.timeoutMs ?? 3e4;
1128
+ this.retry = config2.retry ?? {};
1129
+ this.extraHeaders = config2.headers ?? {};
1130
+ this.fetchFn = config2.fetch ?? globalThis.fetch.bind(globalThis);
1131
+ }
1132
+ async resolveAuth() {
1133
+ if (this.authorization === void 0) return void 0;
1134
+ if (typeof this.authorization === "string") return this.authorization;
1135
+ return this.authorization();
1136
+ }
1137
+ async buildHeaders() {
1138
+ const headers = {
1139
+ "Content-Type": "application/json",
1140
+ ...this.extraHeaders
1141
+ };
1142
+ const auth = await this.resolveAuth();
1143
+ if (auth !== void 0) {
1144
+ headers["Authorization"] = auth;
1145
+ }
1146
+ return headers;
1147
+ }
1148
+ async executeRequest(body) {
1149
+ return executeWithRetry(async () => {
1150
+ const controller = new AbortController();
1151
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1152
+ let response;
1153
+ try {
1154
+ response = await this.fetchFn(this.url, {
1155
+ method: "POST",
1156
+ headers: await this.buildHeaders(),
1157
+ body,
1158
+ signal: controller.signal
1159
+ });
1160
+ } catch (error) {
1161
+ if (error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("abort"))) {
1162
+ throw new TimeoutError();
1163
+ }
1164
+ throw new NetworkError(
1165
+ error instanceof Error ? error.message : "Network request failed",
1166
+ { cause: error }
1167
+ );
1168
+ } finally {
1169
+ clearTimeout(timer);
1170
+ }
1171
+ if (response.status === 401 || response.status === 403) {
1172
+ throw new AuthenticationError(response.status);
1173
+ }
1174
+ if (response.status === 429) {
1175
+ const retryAfterHeader = response.headers.get("Retry-After");
1176
+ const retryAfterMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : void 0;
1177
+ throw new RateLimitError(
1178
+ Number.isNaN(retryAfterMs) ? void 0 : retryAfterMs
1179
+ );
1180
+ }
1181
+ if (!response.ok) {
1182
+ throw new NetworkError(
1183
+ `HTTP ${response.status}: ${response.statusText}`
1184
+ );
1185
+ }
1186
+ let json;
1187
+ try {
1188
+ json = await response.json();
1189
+ } catch (error) {
1190
+ throw new NetworkError("Failed to parse JSON response", {
1191
+ cause: error
1192
+ });
1193
+ }
1194
+ if (json.errors !== null && json.errors !== void 0 && json.errors.length > 0) {
1195
+ throw new GraphQLError(json.errors);
1196
+ }
1197
+ return json.data ?? {};
1198
+ }, this.retry);
1199
+ }
1200
+ async query(query, variables, operationName) {
1201
+ const body = JSON.stringify({
1202
+ query,
1203
+ variables,
1204
+ ...operationName && { operationName }
1205
+ });
1206
+ return this.executeRequest(body);
1207
+ }
1208
+ async mutate(mutation, variables, operationName) {
1209
+ const body = JSON.stringify({
1210
+ query: mutation,
1211
+ variables,
1212
+ ...operationName && { operationName }
1213
+ });
1214
+ return this.executeRequest(body);
1215
+ }
1216
+ };
1217
+
1218
+ // src/index.ts
1219
+ var version = "2.0.0-alpha.1";
1220
+ export {
1221
+ AuthenticationError,
1222
+ CustomScalar,
1223
+ DEFAULT_RETRY_CONFIG,
1224
+ FraiseQLClient,
1225
+ FraiseQLError,
1226
+ GraphQLError,
1227
+ Mutation,
1228
+ NetworkError,
1229
+ Observer,
1230
+ Query,
1231
+ RateLimitError,
1232
+ SCALAR_NAMES,
1233
+ Scalar,
1234
+ ScalarValidationError,
1235
+ SchemaRegistry,
1236
+ Subscription,
1237
+ TimeoutError,
1238
+ Type,
1239
+ config,
1240
+ email,
1241
+ enum_,
1242
+ executeWithRetry,
1243
+ exportSchema,
1244
+ exportSchemaToString,
1245
+ exportTypes,
1246
+ extractFieldInfo,
1247
+ extractFunctionSignature,
1248
+ field,
1249
+ generateCrudOperations,
1250
+ getAllCustomScalars,
1251
+ getSchemaDict,
1252
+ input,
1253
+ interface_,
1254
+ isScalarType,
1255
+ registerMutation,
1256
+ registerQuery,
1257
+ registerSubscription,
1258
+ registerTypeFields,
1259
+ slack,
1260
+ typeToGraphQL,
1261
+ union,
1262
+ validateCustomScalar,
1263
+ version,
1264
+ webhook
1265
+ };