@an5/orm 1.2.0 → 1.3.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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/generator/src/api.d.ts +3 -0
  3. package/dist/generator/src/api.js +7 -1
  4. package/dist/generator/src/code-generator.js +12 -1
  5. package/dist/generator/src/code-request.d.ts +2 -2
  6. package/dist/generator/src/code-request.js +24 -3
  7. package/dist/generator/src/config.d.ts +18 -0
  8. package/dist/generator/src/config.js +24 -0
  9. package/dist/generator/src/dialect.js +4 -2
  10. package/dist/generator/src/dotnet-generator.js +97 -1
  11. package/dist/generator/src/field-types.js +3 -0
  12. package/dist/generator/src/golang-generator.d.ts +8 -0
  13. package/dist/generator/src/golang-generator.js +183 -9
  14. package/dist/generator/src/index.js +18 -0
  15. package/dist/generator/src/java-generator.d.ts +56 -0
  16. package/dist/generator/src/java-generator.js +1408 -0
  17. package/dist/generator/src/kotlin-generator.d.ts +34 -0
  18. package/dist/generator/src/kotlin-generator.js +753 -0
  19. package/dist/generator/src/python-generator.js +4 -2
  20. package/dist/generator/src/rust-generator.js +36 -1
  21. package/dist/generator/src/swift-generator.d.ts +42 -0
  22. package/dist/generator/src/swift-generator.js +768 -0
  23. package/generator/src/api.ts +3 -0
  24. package/generator/src/code-generator.ts +13 -1
  25. package/generator/src/code-request.ts +17 -3
  26. package/generator/src/config.ts +48 -0
  27. package/generator/src/dialect.ts +5 -3
  28. package/generator/src/dotnet-generator.ts +97 -1
  29. package/generator/src/field-types.ts +3 -0
  30. package/generator/src/golang-generator.ts +184 -9
  31. package/generator/src/index.ts +21 -0
  32. package/generator/src/java-generator.ts +1459 -0
  33. package/generator/src/kotlin-generator.ts +798 -0
  34. package/generator/src/python-generator.ts +4 -2
  35. package/generator/src/rust-generator.ts +37 -1
  36. package/generator/src/swift-generator.ts +830 -0
  37. package/package.json +2 -2
@@ -0,0 +1,768 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SwiftGenerator = void 0;
7
+ /**
8
+ * The Swift client generator.
9
+ *
10
+ * Emits `an5Client/swift` sources that depend on the runtime in `an5Adapters/swift`:
11
+ * a `struct` per model, typed filter builders, the metadata the runtime reads its tables
12
+ * and columns from, and an `An5Db` that hands out one client per model.
13
+ *
14
+ * Models are value types with an explicit `init(row:)` rather than anything Codable: the
15
+ * runtime's rows are keyed by column label and carry their own nullability, and a Codable
16
+ * conformance would have to invent a second shape that agrees with neither.
17
+ */
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const type_kinds_1 = require("./type-kinds");
21
+ /** `DECIMAL(10,2)` → `DECIMAL`. */
22
+ function declaredBase(sqlType) {
23
+ return sqlType.replace(/\([^)]*\)/g, ' ').trim().replace(/\s+/g, ' ').toUpperCase();
24
+ }
25
+ /** Acronyms that must keep their capitals when a name becomes camelCase. */
26
+ const ACRONYMS = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
27
+ /** Lower-cased words that would collide with a `CaseIterable` member or read oddly. */
28
+ const SWIFT_KEYWORDS = new Set([
29
+ 'associatedtype', 'class', 'deinit', 'enum', 'extension', 'fileprivate', 'func', 'import',
30
+ 'init', 'inout', 'internal', 'let', 'open', 'operator', 'private', 'protocol', 'public',
31
+ 'rethrows', 'static', 'struct', 'subscript', 'typealias', 'var', 'break', 'case', 'continue',
32
+ 'default', 'defer', 'do', 'else', 'fallthrough', 'for', 'guard', 'if', 'in', 'repeat',
33
+ 'return', 'switch', 'where', 'while', 'as', 'catch', 'false', 'is', 'nil', 'super', 'self',
34
+ 'throw', 'throws', 'true', 'try',
35
+ ]);
36
+ class SwiftGenerator {
37
+ constructor(outputDir) {
38
+ this.outputDir = outputDir;
39
+ if (!fs_1.default.existsSync(this.outputDir)) {
40
+ fs_1.default.mkdirSync(this.outputDir, { recursive: true });
41
+ }
42
+ }
43
+ generate(models) {
44
+ this.provider = models[0]?.provider;
45
+ this.generatePackageManifest();
46
+ this.generateModels(models);
47
+ this.generateOrmTypes(models);
48
+ this.generateMetadata(models);
49
+ this.generateConfig();
50
+ this.generateClient(models);
51
+ }
52
+ /**
53
+ * The SwiftPM manifest for the generated client.
54
+ *
55
+ * <p>SwiftPM has no "point a build at this folder" story the way a csproj or a go.mod
56
+ * does: a target's sources have to live under `Sources/<Target>/`. Emitting the manifest
57
+ * and that layout together is what makes the output a package an app can depend on,
58
+ * rather than a directory a consumer has to restructure by hand.
59
+ */
60
+ generatePackageManifest() {
61
+ fs_1.default.mkdirSync(this.sourceDir, { recursive: true });
62
+ const content = `// swift-tools-version: 5.9
63
+ // This file is auto-generated. Do not edit deliberately.
64
+ import PackageDescription
65
+
66
+ let package = Package(
67
+ name: "An5Client",
68
+ products: [
69
+ .library(name: "An5Client", targets: ["An5Client"])
70
+ ],
71
+ dependencies: [
72
+ .package(url: "https://github.com/an5ORM/an5Adapters.git", from: "${ADAPTER_VERSION}")
73
+ ],
74
+ targets: [
75
+ .target(name: "An5Client", dependencies: [
76
+ .product(name: "An5Adapters", package: "an5Adapters")
77
+ ])
78
+ ]
79
+ )
80
+ `;
81
+ fs_1.default.writeFileSync(path_1.default.join(this.outputDir, 'Package.swift'), content);
82
+ }
83
+ /** Where SwiftPM expects a target's sources. */
84
+ get sourceDir() {
85
+ return path_1.default.join(this.outputDir, 'Sources', 'An5Client');
86
+ }
87
+ // ─── Naming ─────────────────────────────────────────────────────────────────────
88
+ capitalize(value) {
89
+ return value ? value.charAt(0).toUpperCase() + value.slice(1) : value;
90
+ }
91
+ /**
92
+ * The property name for a column.
93
+ *
94
+ * Backticked rather than renamed where possible: `default` and `class` are valid column
95
+ * names, and renaming them in the model but not in the SQL is a mismatch nobody notices.
96
+ */
97
+ property(name) {
98
+ const camel = SwiftGenerator.toCamelCase(name);
99
+ return SWIFT_KEYWORDS.has(camel) ? `\`${camel}\`` : camel;
100
+ }
101
+ static toCamelCase(value) {
102
+ if (!value)
103
+ return value;
104
+ const spaced = ACRONYMS.reduce((text, acronym) => text.replace(new RegExp(`\\b${acronym}\\b`, 'g'), ` ${acronym} `), value);
105
+ const words = spaced.split(/[\s_\-.]+/).filter(Boolean);
106
+ if (words.length === 0)
107
+ return value;
108
+ const [first, ...rest] = words;
109
+ return (first.charAt(0).toLowerCase() +
110
+ first.slice(1) +
111
+ rest.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(''));
112
+ }
113
+ static toLowerCamelCase(value) {
114
+ return SwiftGenerator.toCamelCase(value);
115
+ }
116
+ // ─── Types ───────────────────────────────────────────────────────────────────────
117
+ /** The Swift type for a column. */
118
+ mapType(field) {
119
+ const base = declaredBase(field.sqlType ?? field.type);
120
+ switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
121
+ case 'int':
122
+ return 'Int';
123
+ case 'bigint':
124
+ return 'Int64';
125
+ case 'float':
126
+ // `DECIMAL`/`NUMERIC`/`MONEY` stay exact; Swift's `Double` would round money.
127
+ return ['DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY', 'FIXED'].includes(base)
128
+ ? 'Decimal'
129
+ : 'Double';
130
+ case 'bool':
131
+ return 'Bool';
132
+ case 'date':
133
+ if (base === 'DATE')
134
+ return 'Date';
135
+ return 'Date';
136
+ case 'bytes':
137
+ return 'Data';
138
+ case 'vector':
139
+ return '[Double]';
140
+ case 'json':
141
+ return 'String';
142
+ default:
143
+ return 'String';
144
+ }
145
+ }
146
+ mapOptionalType(field) {
147
+ const mapped = this.mapType(field);
148
+ return `${mapped}?`;
149
+ }
150
+ /** How a column is read out of a row, as a Swift expression. */
151
+ readExpression(field) {
152
+ const base = declaredBase(field.sqlType ?? field.type);
153
+ switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
154
+ case 'int':
155
+ return `row.int("${field.name}")`;
156
+ case 'bigint':
157
+ return `row.int64("${field.name}")`;
158
+ case 'float':
159
+ return ['DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY', 'FIXED'].includes(base)
160
+ ? `row.decimal("${field.name}")`
161
+ : `row.double("${field.name}")`;
162
+ case 'bool':
163
+ return `row.bool("${field.name}")`;
164
+ case 'date':
165
+ return `row.date("${field.name}")`;
166
+ case 'bytes':
167
+ return `row.data("${field.name}")`;
168
+ case 'vector':
169
+ // A `VECTOR(n)` column is nullable like any other column, so the generated
170
+ // property is `[Double]?` and a row without one reads as `nil`.
171
+ return `row.vector("${field.name}")`;
172
+ default:
173
+ return `row.string("${field.name}")`;
174
+ }
175
+ }
176
+ filterType(field) {
177
+ switch ((0, type_kinds_1.fieldKind)(field, this.provider)) {
178
+ case 'date':
179
+ return 'DateFilter';
180
+ case 'bool':
181
+ return 'BoolFilter';
182
+ case 'int':
183
+ case 'bigint':
184
+ case 'float':
185
+ return 'NumberFilter';
186
+ default:
187
+ return 'StringFilter';
188
+ }
189
+ }
190
+ // ─── Models ──────────────────────────────────────────────────────────────────────
191
+ generateModels(models) {
192
+ for (const model of models) {
193
+ fs_1.default.writeFileSync(path_1.default.join(this.sourceDir, `${model.name}.swift`), this.modelSource(model));
194
+ }
195
+ }
196
+ modelSource(model) {
197
+ const name = model.name;
198
+ let content = `// This file is auto-generated. Do not edit deliberately.
199
+ import Foundation
200
+ import An5Adapters
201
+
202
+ /// ${model.description ? escapeDoc(model.description) : `The \`${name}\` model.`}
203
+ public struct ${name} {
204
+ `;
205
+ for (const field of model.fields) {
206
+ const property = this.property(field.name);
207
+ const type = this.mapOptionalType(field);
208
+ content += ` /// \`${field.sqlType}\`${field.isId ? ', primary key' : ''}${field.description ? ` — ${escapeDoc(field.description)}` : ''}\n`;
209
+ content += ` public let ${property}: ${type}\n`;
210
+ }
211
+ for (const rel of model.relations) {
212
+ const property = this.property(rel.name);
213
+ const type = rel.isArray ? `[${rel.type}]` : `${rel.type}?`;
214
+ content += `\n /// Eager-loaded with \`include: ["${rel.name}": true]\`.\n`;
215
+ content += ` public var ${property}: ${type} = ${rel.isArray ? '[]' : 'nil'}\n`;
216
+ }
217
+ content += `\n public init(\n`;
218
+ content += model.fields
219
+ .map((field) => ` ${this.property(field.name)}: ${this.mapOptionalType(field)} = nil${field === model.fields[model.fields.length - 1] ? '' : ','}`)
220
+ .join('\n');
221
+ content += `\n ) {\n`;
222
+ content += model.fields
223
+ .map((field) => ` self.${this.property(field.name)} = ${this.property(field.name)}`)
224
+ .join('\n');
225
+ content += `\n }\n\n`;
226
+ content += ` /// Reads \`${name}\` from a database row.\n`;
227
+ content += ` ///\n`;
228
+ content += ` /// Every column goes through a converter rather than a cast: a \`BOOL\` arrives as an\n`;
229
+ content += ` /// \`Int\` on one driver and \`Bool\` on another, and a \`NUMERIC\` column still arrives\n`;
230
+ content += ` /// as a \`Decimal\` even when it would fit in an \`Int\`.\n`;
231
+ content += ` ///\n`;
232
+ content += ` /// Eager-loaded relations arrive as nested rows and are converted the same way; one\n`;
233
+ content += ` /// that was not asked for keeps its default, so an empty \`orders\` does not say\n`;
234
+ content += ` /// whether the query included it.\n`;
235
+ content += ` public init(row: Row) {\n`;
236
+ content += model.fields
237
+ .map((field) => ` self.${this.property(field.name)} = ${this.readExpression(field)}`)
238
+ .join('\n');
239
+ if (model.relations.length > 0) {
240
+ content += '\n';
241
+ for (const rel of model.relations) {
242
+ const property = this.property(rel.name);
243
+ if (rel.isArray) {
244
+ content += ` self.${property} = row.related("${rel.name}").map { ${rel.type}(row: $0) }\n`;
245
+ }
246
+ else {
247
+ content += ` self.${property} = row.relatedOne("${rel.name}").map { ${rel.type}(row: $0) }\n`;
248
+ }
249
+ }
250
+ }
251
+ content += `\n }\n\n`;
252
+ content += ` /// The columns to write, in declaration order, with \`nil\` left out.\n`;
253
+ content += ` ///\n`;
254
+ content += ` /// An unset column takes the schema's DEFAULT, which is what leaving it out means —\n`;
255
+ content += ` /// and what \`update\` needs so a partial value does not blank every other column.\n`;
256
+ content += ` public var values: Values {\n`;
257
+ content += ` var values = Values()\n`;
258
+ content += model.fields
259
+ .map((field) => ` if let value = self.${this.property(field.name)} { values["${field.name}"] = value }`)
260
+ .join('\n');
261
+ content += `\n return values\n }\n}\n`;
262
+ return content;
263
+ }
264
+ // ─── ORM types ───────────────────────────────────────────────────────────────────
265
+ generateOrmTypes(models) {
266
+ let content = `// This file is auto-generated. Do not edit deliberately.
267
+ import Foundation
268
+ import An5Adapters
269
+
270
+ /// Typed filters for the generated models.
271
+ ///
272
+ /// A filter is a small builder whose \`build()\` produces the filter tree the runtime's SQL
273
+ /// builder reads, so a query is checked at the call site and still compiles down to bind
274
+ /// parameters rather than interpolated SQL.
275
+ public enum An5Orm {
276
+
277
+ /// A filter on a string column.
278
+ public struct StringFilter {
279
+ private var operators: Where = [:]
280
+
281
+ public static func \`is\`(_ value: String?) -> StringFilter {
282
+ var filter = StringFilter()
283
+ filter.operators["equals"] = value
284
+ return filter
285
+ }
286
+
287
+ public static func isNot(_ value: String?) -> StringFilter {
288
+ var filter = StringFilter()
289
+ filter.operators["not"] = value
290
+ return filter
291
+ }
292
+
293
+ public static func has(_ value: String) -> StringFilter {
294
+ var filter = StringFilter()
295
+ filter.operators["contains"] = value
296
+ return filter
297
+ }
298
+
299
+ public static func starts(_ value: String) -> StringFilter {
300
+ var filter = StringFilter()
301
+ filter.operators["startsWith"] = value
302
+ return filter
303
+ }
304
+
305
+ public static func ends(_ value: String) -> StringFilter {
306
+ var filter = StringFilter()
307
+ filter.operators["endsWith"] = value
308
+ return filter
309
+ }
310
+
311
+ public func inList(_ values: [String]) -> StringFilter {
312
+ var filter = self
313
+ filter.operators["in"] = values
314
+ return filter
315
+ }
316
+
317
+ public func notInList(_ values: [String]) -> StringFilter {
318
+ var filter = self
319
+ filter.operators["notIn"] = values
320
+ return filter
321
+ }
322
+
323
+ /// The filter as the runtime reads it.
324
+ public func build() -> Where { operators }
325
+ }
326
+
327
+ /// A filter on an integer or floating-point column.
328
+ public struct NumberFilter {
329
+ private var operators: Where = [:]
330
+
331
+ public static func \`is\`(_ value: Double?) -> NumberFilter {
332
+ var filter = NumberFilter()
333
+ filter.operators["equals"] = value
334
+ return filter
335
+ }
336
+
337
+ public static func atLeast(_ value: Double?) -> NumberFilter {
338
+ var filter = NumberFilter()
339
+ filter.operators["gte"] = value
340
+ return filter
341
+ }
342
+
343
+ public static func atMost(_ value: Double?) -> NumberFilter {
344
+ var filter = NumberFilter()
345
+ filter.operators["lte"] = value
346
+ return filter
347
+ }
348
+
349
+ public static func greater(_ value: Double?) -> NumberFilter {
350
+ var filter = NumberFilter()
351
+ filter.operators["gt"] = value
352
+ return filter
353
+ }
354
+
355
+ public static func less(_ value: Double?) -> NumberFilter {
356
+ var filter = NumberFilter()
357
+ filter.operators["lt"] = value
358
+ return filter
359
+ }
360
+
361
+ public func inList(_ values: [Double]) -> NumberFilter {
362
+ var filter = self
363
+ filter.operators["in"] = values
364
+ return filter
365
+ }
366
+
367
+ /// The filter as the runtime reads it.
368
+ public func build() -> Where { operators }
369
+ }
370
+
371
+ /// A filter on a boolean column.
372
+ public struct BoolFilter {
373
+ private var operators: Where = [:]
374
+
375
+ public static func \`is\`(_ value: Bool?) -> BoolFilter {
376
+ var filter = BoolFilter()
377
+ filter.operators["equals"] = value
378
+ return filter
379
+ }
380
+
381
+ /// The filter as the runtime reads it.
382
+ public func build() -> Where { operators }
383
+ }
384
+
385
+ /// A filter on a date column.
386
+ public struct DateFilter {
387
+ private var operators: Where = [:]
388
+
389
+ public static func \`is\`(_ value: Date?) -> DateFilter {
390
+ var filter = DateFilter()
391
+ filter.operators["equals"] = value
392
+ return filter
393
+ }
394
+
395
+ public static func atLeast(_ value: Date?) -> DateFilter {
396
+ var filter = DateFilter()
397
+ filter.operators["gte"] = value
398
+ return filter
399
+ }
400
+
401
+ public static func atMost(_ value: Date?) -> DateFilter {
402
+ var filter = DateFilter()
403
+ filter.operators["lte"] = value
404
+ return filter
405
+ }
406
+
407
+ /// The filter as the runtime reads it.
408
+ public func build() -> Where { operators }
409
+ }
410
+ `;
411
+ for (const model of models) {
412
+ const name = model.name;
413
+ content += `
414
+ /// A \`WHERE\` for ${name}. Unset filters are left out.
415
+ public struct ${name}Where {
416
+ private var operators: Where = [:]
417
+ ${model.fields.map((field) => ` public var ${this.property(field.name)}: ${this.filterType(field)}?`).join('\n')}
418
+
419
+ /// Creates a filter with every column unset; assign the ones you want to match.
420
+ public init() {}
421
+
422
+ public func and(_ clauses: ${name}Where...) -> ${name}Where {
423
+ var filter = self
424
+ filter.operators["AND"] = clauses.map { $0.build() }
425
+ return filter
426
+ }
427
+
428
+ public func or(_ clauses: ${name}Where...) -> ${name}Where {
429
+ var filter = self
430
+ filter.operators["OR"] = clauses.map { $0.build() }
431
+ return filter
432
+ }
433
+
434
+ public func not(_ clause: ${name}Where) -> ${name}Where {
435
+ var filter = self
436
+ filter.operators["NOT"] = [clause.build()]
437
+ return filter
438
+ }
439
+
440
+ /// The filter tree as the runtime reads it.
441
+ ///
442
+ /// Column conditions are read from the properties here rather than written into
443
+ /// \`operators\` as they are assigned, because a condition dropped on the way in is not
444
+ /// an error — it is a filter that quietly matches every row.
445
+ public func build() -> Where {
446
+ var filter = operators
447
+ ${model.fields
448
+ .map((field) => {
449
+ const property = this.property(field.name);
450
+ return ` if let ${property} = ${property} { filter["${field.name}"] = ${property}.build() }`;
451
+ })
452
+ .join('\n')}
453
+ return filter
454
+ }
455
+ }
456
+
457
+ /// An \`ORDER BY\` for ${name}.
458
+ public struct ${name}OrderBy {
459
+ private var entries: [[String: String]] = []
460
+
461
+ public init() {}
462
+
463
+ /// Orders by the columns, ascending.
464
+ public func asc(_ columns: String...) -> ${name}OrderBy {
465
+ var order = self
466
+ for column in columns { order.entries.append([column: "asc"]) }
467
+ return order
468
+ }
469
+
470
+ /// Orders by the column, descending.
471
+ public func desc(_ column: String) -> ${name}OrderBy {
472
+ var order = self
473
+ order.entries.append([column: "desc"])
474
+ return order
475
+ }
476
+
477
+ /// The sort as the runtime reads it.
478
+ public func build() -> [[String: String]] { entries }
479
+ }
480
+ `;
481
+ }
482
+ content += `}\n`;
483
+ fs_1.default.writeFileSync(path_1.default.join(this.sourceDir, 'An5OrmTypes.swift'), content);
484
+ }
485
+ // ─── Metadata ────────────────────────────────────────────────────────────────────
486
+ generateMetadata(models) {
487
+ let content = `// This file is auto-generated. Do not edit deliberately.
488
+ import Foundation
489
+ import An5Adapters
490
+
491
+ /// The generated models' tables, columns and relations.
492
+ ///
493
+ /// Passed to the runtime when a database is opened. Without it the table clients have no
494
+ /// table names to work with and no primary key to fill in.
495
+ public enum An5Metadata {
496
+
497
+ /// Model name to table name, schema-qualified.
498
+ public static let modelToTable: [String: String] = [
499
+ ${models.map((model) => ` "${model.name}": "${this.tableName(model)}",`).join('\n')}
500
+ ]
501
+
502
+ /// Model name to its columns, as the runtime reads them.
503
+ public static let modelFields: [String: [[String: Any?]]] = [
504
+ ${models
505
+ .map((model) => ` "${model.name}": [
506
+ ${model.fields
507
+ .map((field) => ` ["name": "${field.name}", "type": "${field.type}", "sql": "${field.sqlType}", "isOptional": ${field.isOptional}, "hasDefault": ${field.hasDefault}, "isId": ${field.isId}${field.description ? `, "description": ${swiftString(field.description)}` : ''}],`)
508
+ .join('\n')}
509
+ ],`)
510
+ .join('\n')}
511
+ ]
512
+
513
+ /// Model name to its relations, keyed by relation name.
514
+ public static let relationMap: [String: [String: [String: String]]] = ${models.some((model) => model.relations.length > 0) ? '[' : '[:]'}
515
+ ${models.some((model) => model.relations.length > 0)
516
+ ? models
517
+ .filter((model) => model.relations.length > 0)
518
+ .map((model) => ` "${model.name}": [
519
+ ${model.relations
520
+ .map((rel) => ` "${rel.name}": ["modelName": "${rel.type}", "relationType": "${rel.relationName}", "foreignKey": "${rel.foreignKey}", "localKey": "${rel.localKey}"],`)
521
+ .join('\n')}
522
+ ],`)
523
+ .join('\n') + '\n ]'
524
+ : ''}
525
+
526
+ /// This schema, in the shape the runtime reads.
527
+ public static var metadata: Metadata {
528
+ Metadata([
529
+ "modelToTable": modelToTable,
530
+ "modelFields": modelFields,
531
+ "relationMap": relationMap,
532
+ ]) ?? .empty
533
+ }
534
+ }
535
+ `;
536
+ fs_1.default.writeFileSync(path_1.default.join(this.sourceDir, 'An5Metadata.swift'), content);
537
+ }
538
+ // ─── Config ──────────────────────────────────────────────────────────────────────
539
+ generateConfig() {
540
+ const content = `// This file is auto-generated. Do not edit deliberately.
541
+ import Foundation
542
+ import An5Adapters
543
+
544
+ /// Where the connection string comes from.
545
+ ///
546
+ /// The \`AN5_DATABASE_URL\` environment variable first, then the \`AN5_CONNECTION_STRING\`
547
+ /// Info.plist entry an app bundle carries, so one build can be pointed at a different
548
+ /// database without a recompile.
549
+ public enum An5Config {
550
+
551
+ public static func connectionString() throws -> String {
552
+ if let fromEnvironment = ProcessInfo.processInfo.environment["AN5_DATABASE_URL"],
553
+ !fromEnvironment.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
554
+ return fromEnvironment.trimmingCharacters(in: .whitespacesAndNewlines)
555
+ }
556
+ if let fromBundle = Bundle.main.object(forInfoDictionaryKey: "AN5_CONNECTION_STRING") as? String,
557
+ !fromBundle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
558
+ return fromBundle.trimmingCharacters(in: .whitespacesAndNewlines)
559
+ }
560
+ throw An5Error.configuration(
561
+ "no connection string: set AN5_DATABASE_URL or the AN5_CONNECTION_STRING Info.plist entry")
562
+ }
563
+ }
564
+ `;
565
+ fs_1.default.writeFileSync(path_1.default.join(this.sourceDir, 'An5Config.swift'), content);
566
+ }
567
+ // ─── Client ──────────────────────────────────────────────────────────────────────
568
+ generateClient(models) {
569
+ let accessors = models.map((model) => ({
570
+ property: SwiftGenerator.toLowerCamelCase(model.name),
571
+ name: model.name,
572
+ }));
573
+ const unique = new Map();
574
+ for (const entry of accessors) {
575
+ const key = SwiftGenerator.toLowerCamelCase(entry.name);
576
+ if (!unique.has(key))
577
+ unique.set(key, { property: key, name: entry.name });
578
+ }
579
+ let content = `// This file is auto-generated. Do not edit deliberately.
580
+ import Foundation
581
+ import An5Adapters
582
+
583
+ /// A typed client for one model.
584
+ ///
585
+ /// The runtime's \`TableClient\` works on untyped rows so it stays independent of the
586
+ /// schema; this adds the model's own type on top, which is what makes
587
+ /// \`db.${Array.from(unique.values())[0]?.property ?? 'user'}.findMany()\` come back as
588
+ /// \`[${models[0]?.name ?? 'User'}]\` rather than \`[Row]\`.
589
+ public struct ModelClient<T> {
590
+
591
+ private let table: TableClient
592
+ private let read: (Row) -> T
593
+ private let write: (T) -> Values
594
+
595
+ init(table: TableClient, read: @escaping (Row) -> T, write: @escaping (T) -> Values) {
596
+ self.table = table
597
+ self.read = read
598
+ self.write = write
599
+ }
600
+
601
+ /// The model this client reads and writes.
602
+ public var model: String { table.model }
603
+
604
+ public func findMany() throws -> [T] { try table.findMany().map { read($0) } }
605
+
606
+ public func findMany(_ query: Query) throws -> [T] { try table.findMany(query).map { read($0) } }
607
+
608
+ public func findMany(filter: Where) throws -> [T] { try table.findMany(filter: filter).map { read($0) } }
609
+
610
+ public func findFirst(_ query: Query = Query()) throws -> T? { try table.findFirst(query).map { read($0) } }
611
+
612
+ public func findFirst(filter: Where) throws -> T? { try table.findFirst(filter: filter).map { read($0) } }
613
+
614
+ public func findUnique(filter: Where) throws -> T? { try table.findUnique(filter: filter).map { read($0) } }
615
+
616
+ public func count(_ filter: Where? = nil) throws -> Int { try table.count(filter) }
617
+
618
+ /// Inserts a value and returns it as stored.
619
+ @discardableResult
620
+ public func create(_ value: T) throws -> T { read(try table.create(write(value))) }
621
+
622
+ /// Inserts a value and returns it with the relations the query asked for.
623
+ @discardableResult
624
+ public func create(_ value: T, _ query: Query) throws -> T { read(try table.create(write(value), query)) }
625
+
626
+ /// Inserts many values, one statement each.
627
+ @discardableResult
628
+ public func createMany(_ values: [T], skipDuplicates: Bool = false) throws -> Int {
629
+ try table.createMany(values.map(write), skipDuplicates: skipDuplicates)
630
+ }
631
+
632
+ /// Updates the matching rows and returns the first of them re-read.
633
+ @discardableResult
634
+ public func update(filter: Where, data: T) throws -> T? { try table.update(filter: filter, data: write(data)).map { read($0) } }
635
+
636
+ /// Updates every matching row and gives back how many changed.
637
+ @discardableResult
638
+ public func updateMany(filter: Where?, data: T) throws -> Int { try table.updateMany(filter: filter, data: write(data)) }
639
+
640
+ /// Deletes the matching rows and returns the one that was there first.
641
+ @discardableResult
642
+ public func delete(filter: Where) throws -> T? { try table.delete(filter: filter).map { read($0) } }
643
+
644
+ /// Deletes every matching row, or the whole table when \`filter\` is \`nil\`.
645
+ @discardableResult
646
+ public func deleteMany(filter: Where? = nil) throws -> Int { try table.deleteMany(filter: filter) }
647
+
648
+ /// Updates the matching row when it exists, creates it otherwise.
649
+ @discardableResult
650
+ public func upsert(filter: Where, create: T, update: T) throws -> T {
651
+ read(try table.upsert(filter: filter, create: write(create), update: write(update)))
652
+ }
653
+
654
+ /// One row of aggregate values, keyed \`_count\`, \`_sum_<field>\` and so on.
655
+ public func aggregate(_ aggregate: Aggregate) throws -> Row { try table.aggregate(aggregate) }
656
+
657
+ /// One row per group, each carrying the group's count and aggregates.
658
+ public func groupBy(_ aggregate: Aggregate) throws -> [Row] { try table.groupBy(aggregate) }
659
+
660
+ /// The rows nearest a vector.
661
+ public func vectorSearch(
662
+ _ vector: [Double],
663
+ take: Int = 10,
664
+ filter: Where? = nil,
665
+ vectorField: String = "embedding",
666
+ metric: DistanceMetric = .cosine
667
+ ) throws -> [T] {
668
+ try table.vectorSearch(vector, take: take, filter: filter, vectorField: vectorField, metric: metric).map { read($0) }
669
+ }
670
+ }
671
+
672
+ /// The AN5 entry point: one typed client per model, over one connection.
673
+ ///
674
+ /// \`\`\`
675
+ /// let db = try An5Db(path: ":memory:")
676
+ /// let ada = try db.user.findUnique(filter: ["name": "Ada"])
677
+ /// \`\`\`
678
+ public final class An5Db {
679
+
680
+ /// The runtime underneath, for raw SQL and for anything the typed clients miss.
681
+ public let an5: An5Adapter
682
+
683
+ ${Array.from(unique.values())
684
+ .map((entry) => ` /// Queries for \`${entry.name}\`.\n public let ${entry.property}: ModelClient<${entry.name}>`)
685
+ .join('\n\n')}
686
+
687
+ /// Opens a database from a connection string, registering this schema with the runtime.
688
+ public convenience init(connectionString: String) throws {
689
+ let driver = try SQLiteDriver(path: An5Db.path(connectionString))
690
+ self.init(adapter: An5Adapter(driver: driver,
691
+ connectionString: connectionString,
692
+ metadata: An5Metadata.metadata))
693
+ }
694
+
695
+ /// Opens an in-memory or on-disk database, registering this schema with the runtime.
696
+ public convenience init(path: String) throws {
697
+ let driver = try SQLiteDriver(path: path)
698
+ self.init(adapter: An5Adapter(driver: driver,
699
+ connectionString: "sqlite::memory:",
700
+ metadata: An5Metadata.metadata))
701
+ }
702
+
703
+ /// Wraps a runtime the caller already opened.
704
+ public init(adapter: An5Adapter) {
705
+ self.an5 = adapter
706
+ ${Array.from(unique.values())
707
+ .map((entry) => ` self.${entry.property} = ModelClient(\n table: adapter.table("${entry.name}"),\n read: { ${entry.name}(row: $0) },\n write: { $0.values })`)
708
+ .join('\n')}
709
+ }
710
+
711
+ /// A typed client for a table outside this schema.
712
+ public func table<T>(_ model: String, read: @escaping (Row) -> T, write: @escaping (T) -> Values) -> ModelClient<T> {
713
+ ModelClient(table: an5.table(model), read: read, write: write)
714
+ }
715
+
716
+ /// A read-only client for a database view.
717
+ public func view(_ name: String) -> ViewClient { an5.view(name) }
718
+
719
+ /// The dialect the connection string points at.
720
+ public var dialect: Dialect { an5.dialect }
721
+
722
+ /// Runs a query and returns its rows keyed by column label.
723
+ public func query(_ sql: String, _ parameters: [Any?] = []) throws -> [Row] {
724
+ try an5.query(sql, parameters)
725
+ }
726
+
727
+ /// Runs a statement that returns no rows and gives back the affected row count.
728
+ @discardableResult
729
+ public func execute(_ sql: String, _ parameters: [Any?] = []) throws -> Int {
730
+ try an5.execute(sql, parameters)
731
+ }
732
+
733
+ /// Runs \`body\` inside a transaction, committing on return and rolling back on failure.
734
+ public func transaction<T>(_ body: (An5Adapter) throws -> T) throws -> T {
735
+ try an5.transaction(body)
736
+ }
737
+
738
+ private static func path(_ connectionString: String) -> String {
739
+ for prefix in ["sqlite:///", "sqlite://", "sqlite:"] where connectionString.hasPrefix(prefix) {
740
+ return String(connectionString.dropFirst(prefix.count))
741
+ }
742
+ return connectionString
743
+ }
744
+ }
745
+ `;
746
+ fs_1.default.writeFileSync(path_1.default.join(this.sourceDir, 'An5Db.swift'), content);
747
+ }
748
+ // ─── Helpers ─────────────────────────────────────────────────────────────────────
749
+ tableName(model) {
750
+ return model.schemaName ? `${model.schemaName}.${model.tableName}` : model.tableName;
751
+ }
752
+ }
753
+ exports.SwiftGenerator = SwiftGenerator;
754
+ /** The runtime version the generated client declares a dependency on. */
755
+ const ADAPTER_VERSION = '0.2.11';
756
+ /** A Swift string literal for a schema description. */
757
+ function swiftString(value) {
758
+ return JSON.stringify(value);
759
+ }
760
+ /**
761
+ * Doc-comment text.
762
+ *
763
+ * A comment terminator inside a description would end this comment early and leave the rest
764
+ * of the line as code, so it is split.
765
+ */
766
+ function escapeDoc(value) {
767
+ return value.replace(/\*\//g, '*\\/').replace(/\r?\n/g, ' ');
768
+ }