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/README.md +784 -0
- package/dist/index.d.mts +1688 -0
- package/dist/index.d.ts +1688 -0
- package/dist/index.js +1345 -0
- package/dist/index.mjs +1265 -0
- package/package.json +82 -0
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
|
+
};
|