@syncular/typegen 0.1.2 → 0.2.0

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 (99) hide show
  1. package/README.md +436 -94
  2. package/dist/cli.d.ts +1 -2
  3. package/dist/cli.js +159 -73
  4. package/dist/emit-dart.d.ts +17 -0
  5. package/dist/emit-dart.js +201 -0
  6. package/dist/emit-kotlin.d.ts +16 -0
  7. package/dist/emit-kotlin.js +201 -0
  8. package/dist/emit-queries-dart.d.ts +2 -0
  9. package/dist/emit-queries-dart.js +160 -0
  10. package/dist/emit-queries-kotlin.d.ts +2 -0
  11. package/dist/emit-queries-kotlin.js +162 -0
  12. package/dist/emit-queries-swift.d.ts +2 -0
  13. package/dist/emit-queries-swift.js +146 -0
  14. package/dist/emit-queries.d.ts +2 -0
  15. package/dist/emit-queries.js +129 -0
  16. package/dist/emit-swift.d.ts +18 -0
  17. package/dist/emit-swift.js +235 -0
  18. package/dist/emit.d.ts +16 -0
  19. package/dist/emit.js +191 -0
  20. package/dist/errors.d.ts +6 -0
  21. package/dist/errors.js +10 -0
  22. package/dist/generate.d.ts +53 -18
  23. package/dist/generate.js +391 -77
  24. package/dist/index.d.ts +17 -14
  25. package/dist/index.js +16 -13
  26. package/dist/init.d.ts +12 -0
  27. package/dist/init.js +85 -0
  28. package/dist/ir.d.ts +90 -0
  29. package/dist/ir.js +94 -0
  30. package/dist/manifest.d.ts +76 -0
  31. package/dist/manifest.js +303 -0
  32. package/dist/query.d.ts +96 -0
  33. package/dist/query.js +719 -0
  34. package/dist/sql.d.ts +13 -0
  35. package/dist/sql.js +440 -0
  36. package/package.json +14 -34
  37. package/src/cli.ts +161 -82
  38. package/src/emit-dart.ts +245 -0
  39. package/src/emit-kotlin.ts +257 -0
  40. package/src/emit-queries-dart.ts +203 -0
  41. package/src/emit-queries-kotlin.ts +209 -0
  42. package/src/emit-queries-swift.ts +198 -0
  43. package/src/emit-queries.ts +183 -0
  44. package/src/emit-swift.ts +295 -0
  45. package/src/emit.ts +239 -0
  46. package/src/errors.ts +11 -0
  47. package/src/generate.ts +574 -105
  48. package/src/index.ts +16 -13
  49. package/src/init.ts +100 -0
  50. package/src/ir.ts +197 -0
  51. package/src/manifest.ts +445 -0
  52. package/src/query.ts +918 -0
  53. package/src/sql.ts +533 -0
  54. package/dist/app-contract.d.ts +0 -154
  55. package/dist/app-contract.d.ts.map +0 -1
  56. package/dist/app-contract.js +0 -250
  57. package/dist/app-contract.js.map +0 -1
  58. package/dist/checksums.d.ts +0 -6
  59. package/dist/checksums.d.ts.map +0 -1
  60. package/dist/checksums.js +0 -173
  61. package/dist/checksums.js.map +0 -1
  62. package/dist/cli.d.ts.map +0 -1
  63. package/dist/cli.js.map +0 -1
  64. package/dist/generate.d.ts.map +0 -1
  65. package/dist/generate.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/introspect-postgres.d.ts +0 -8
  69. package/dist/introspect-postgres.d.ts.map +0 -1
  70. package/dist/introspect-postgres.js +0 -94
  71. package/dist/introspect-postgres.js.map +0 -1
  72. package/dist/introspect-sqlite.d.ts +0 -10
  73. package/dist/introspect-sqlite.d.ts.map +0 -1
  74. package/dist/introspect-sqlite.js +0 -97
  75. package/dist/introspect-sqlite.js.map +0 -1
  76. package/dist/introspect.d.ts +0 -8
  77. package/dist/introspect.d.ts.map +0 -1
  78. package/dist/introspect.js +0 -18
  79. package/dist/introspect.js.map +0 -1
  80. package/dist/map-types.d.ts +0 -20
  81. package/dist/map-types.d.ts.map +0 -1
  82. package/dist/map-types.js +0 -151
  83. package/dist/map-types.js.map +0 -1
  84. package/dist/render.d.ts +0 -23
  85. package/dist/render.d.ts.map +0 -1
  86. package/dist/render.js +0 -140
  87. package/dist/render.js.map +0 -1
  88. package/dist/types.d.ts +0 -122
  89. package/dist/types.d.ts.map +0 -1
  90. package/dist/types.js +0 -5
  91. package/dist/types.js.map +0 -1
  92. package/src/app-contract.ts +0 -531
  93. package/src/checksums.ts +0 -257
  94. package/src/introspect-postgres.ts +0 -149
  95. package/src/introspect-sqlite.ts +0 -156
  96. package/src/introspect.ts +0 -36
  97. package/src/map-types.ts +0 -189
  98. package/src/render.ts +0 -196
  99. package/src/types.ts +0 -137
@@ -0,0 +1,201 @@
1
+ /** §2.4 column type → honest Kotlin type. `json`/`blob_ref` are the raw
2
+ * canonical JSON string; `bytes`/`crdt` are opaque `ByteArray`. */
3
+ const KOTLIN_TYPE = {
4
+ string: 'String',
5
+ integer: 'Long',
6
+ float: 'Double',
7
+ boolean: 'Boolean',
8
+ json: 'String',
9
+ bytes: 'ByteArray',
10
+ blob_ref: 'String',
11
+ crdt: 'ByteArray',
12
+ };
13
+ /** §5.11: app-side Kotlin type — declaredType for an encrypted column. */
14
+ function appKotlinType(column) {
15
+ const type = column.encrypted === true && column.declaredType !== undefined
16
+ ? column.declaredType
17
+ : column.type;
18
+ return KOTLIN_TYPE[type];
19
+ }
20
+ function pascalCase(name) {
21
+ return name
22
+ .split(/[_-]+/)
23
+ .filter((part) => part.length > 0)
24
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
25
+ .join('');
26
+ }
27
+ function camelCase(name) {
28
+ const pascal = pascalCase(name);
29
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
30
+ }
31
+ function quote(value) {
32
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
33
+ }
34
+ function emitSchemaValue(ir, indent) {
35
+ const lines = [];
36
+ const i = indent;
37
+ lines.push(`${i}JsonValue.obj(`);
38
+ lines.push(`${i} "version" to JsonValue.of(${ir.schemaVersion}),`);
39
+ lines.push(`${i} "tables" to JsonValue.arr(listOf(`);
40
+ for (const table of ir.tables) {
41
+ lines.push(`${i} JsonValue.obj(`);
42
+ lines.push(`${i} "name" to JsonValue.of(${quote(table.name)}),`);
43
+ lines.push(`${i} "primaryKey" to JsonValue.of(${quote(table.primaryKey)}),`);
44
+ lines.push(`${i} "columns" to JsonValue.arr(listOf(`);
45
+ for (const column of table.columns) {
46
+ const parts = [
47
+ `"name" to JsonValue.of(${quote(column.name)})`,
48
+ `"type" to JsonValue.of(${quote(column.type)})`,
49
+ `"nullable" to JsonValue.of(${column.nullable})`,
50
+ ];
51
+ if (column.crdtType !== undefined) {
52
+ parts.push(`"crdtType" to JsonValue.of(${quote(column.crdtType)})`);
53
+ }
54
+ if (column.encrypted === true) {
55
+ parts.push('"encrypted" to JsonValue.of(true)');
56
+ parts.push(`"declaredType" to JsonValue.of(${quote(column.declaredType ?? column.type)})`);
57
+ }
58
+ lines.push(`${i} JsonValue.obj(${parts.join(', ')}),`);
59
+ }
60
+ lines.push(`${i} )),`);
61
+ lines.push(`${i} "scopes" to JsonValue.arr(listOf(`);
62
+ for (const scope of table.scopes) {
63
+ lines.push(`${i} JsonValue.obj("pattern" to JsonValue.of(${quote(scope.pattern)}), "column" to JsonValue.of(${quote(scope.column)})),`);
64
+ }
65
+ lines.push(`${i} )),`);
66
+ lines.push(`${i} ),`);
67
+ }
68
+ lines.push(`${i} )),`);
69
+ lines.push(`${i})`);
70
+ return lines;
71
+ }
72
+ /** The accessor lifting a §2.4 value out of a row `JsonValue`. */
73
+ function rowAccessor(column) {
74
+ const key = `row[${quote(column.name)}]`;
75
+ switch (column.type) {
76
+ case 'string':
77
+ case 'json':
78
+ case 'blob_ref':
79
+ return `${key}?.string`;
80
+ case 'integer':
81
+ return `${key}?.number?.toLong()`;
82
+ case 'float':
83
+ return `${key}?.number`;
84
+ case 'boolean':
85
+ return `rowBool(${key})`;
86
+ case 'bytes':
87
+ case 'crdt':
88
+ return `rowBytes(${key})`;
89
+ }
90
+ }
91
+ function emitDataClass(table) {
92
+ const type = pascalCase(table.name);
93
+ const lines = [];
94
+ lines.push(`/** One ${table.name} row (§2.4 column order). */`);
95
+ lines.push(`data class ${type}(`);
96
+ for (const column of table.columns) {
97
+ const kotlin = appKotlinType(column);
98
+ const opt = column.nullable ? '?' : '';
99
+ lines.push(` val ${camelCase(column.name)}: ${kotlin}${opt},`);
100
+ }
101
+ lines.push(') {');
102
+ lines.push(' companion object {');
103
+ lines.push(` /** Build from a \`query\`/\`readRows\` row. Null on a missing/mistyped non-nullable column. */`);
104
+ lines.push(` fun fromRow(row: JsonValue): ${type}? {`);
105
+ for (const column of table.columns) {
106
+ const name = camelCase(column.name);
107
+ const accessor = rowAccessor(column);
108
+ if (column.nullable) {
109
+ lines.push(` val ${name} = ${accessor}`);
110
+ }
111
+ else {
112
+ lines.push(` val ${name} = ${accessor} ?: return null`);
113
+ }
114
+ }
115
+ const args = table.columns.map((c) => camelCase(c.name)).join(', ');
116
+ lines.push(` return ${type}(${args})`);
117
+ lines.push(' }');
118
+ lines.push(' }');
119
+ lines.push('}');
120
+ return lines;
121
+ }
122
+ function emitSubscription(sub) {
123
+ const params = [];
124
+ for (const scope of sub.scopes) {
125
+ for (const value of scope.values) {
126
+ if (value.kind === 'parameter' && !params.includes(value.name)) {
127
+ params.push(value.name);
128
+ }
129
+ }
130
+ }
131
+ const lines = [];
132
+ lines.push(` /** Requested-scope template for the ${quote(sub.name)} subscription. */`);
133
+ lines.push(` object ${pascalCase(sub.name)} {`);
134
+ lines.push(` const val name = ${quote(sub.name)}`);
135
+ lines.push(` const val table = ${quote(sub.table)}`);
136
+ const args = params.map((p) => `${camelCase(p)}: String`).join(', ');
137
+ lines.push(` fun scopes(${args}): Map<String, List<String>> = mapOf(`);
138
+ for (const scope of sub.scopes) {
139
+ const values = scope.values
140
+ .map((value) => value.kind === 'literal' ? quote(value.value) : camelCase(value.name))
141
+ .join(', ');
142
+ lines.push(` ${quote(scope.variable)} to listOf(${values}),`);
143
+ }
144
+ lines.push(' )');
145
+ lines.push(' }');
146
+ return lines;
147
+ }
148
+ export function emitKotlinModule(ir, hash, packageName, objectName) {
149
+ const parts = [];
150
+ parts.push([
151
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
152
+ `// irVersion: ${ir.irVersion}`,
153
+ `// irHash: ${hash}`,
154
+ '',
155
+ `package ${packageName}`,
156
+ '',
157
+ 'import dev.syncular.JsonValue',
158
+ ].join('\n'));
159
+ const schemaLines = [];
160
+ schemaLines.push('/** The generated syncular schema (SPEC §2.4, §3.1). */');
161
+ schemaLines.push(`object ${objectName} {`);
162
+ schemaLines.push(` /** ServerSchema-compatible schema — pass to \`SyncularClient.create(schema = …)\`. */`);
163
+ schemaLines.push(' val schema: JsonValue =');
164
+ for (const line of emitSchemaValue(ir, ' '))
165
+ schemaLines.push(line);
166
+ schemaLines.push('');
167
+ schemaLines.push(' /** Typed requested-scope builders per subscription. */');
168
+ schemaLines.push(' object Subscriptions {');
169
+ if (ir.subscriptions.length === 0) {
170
+ schemaLines.push(' // (no subscriptions declared)');
171
+ }
172
+ for (const sub of ir.subscriptions) {
173
+ for (const line of emitSubscription(sub))
174
+ schemaLines.push(line);
175
+ }
176
+ schemaLines.push(' }');
177
+ schemaLines.push('}');
178
+ parts.push(schemaLines.join('\n'));
179
+ // Shared row-decode helpers.
180
+ parts.push([
181
+ '/** Lift a SQLite boolean: a real JSON bool, or 0/1 as a number. */',
182
+ 'private fun rowBool(value: JsonValue?): Boolean? {',
183
+ ' value?.bool?.let { return it }',
184
+ ' value?.number?.let { return it != 0.0 }',
185
+ ' return null',
186
+ '}',
187
+ '',
188
+ '/** Decode the core\'s `{"$bytes":"<hex>"}` marshaling into a ByteArray. */',
189
+ 'private fun rowBytes(value: JsonValue?): ByteArray? {',
190
+ ' val hex = value?.get("\\$bytes")?.string ?: return null',
191
+ ' if (hex.length % 2 != 0) return null',
192
+ ' return ByteArray(hex.length / 2) { i ->',
193
+ ' hex.substring(i * 2, i * 2 + 2).toInt(16).toByte()',
194
+ ' }',
195
+ '}',
196
+ ].join('\n'));
197
+ for (const table of ir.tables) {
198
+ parts.push(emitDataClass(table).join('\n'));
199
+ }
200
+ return `${parts.join('\n\n')}\n`;
201
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalyzedQuery } from './query.js';
2
+ export declare function emitQueriesDartModule(queries: readonly AnalyzedQuery[], hash: string, irVersion: number): string;
@@ -0,0 +1,160 @@
1
+ const DART_TYPE = {
2
+ string: 'String',
3
+ integer: 'int',
4
+ float: 'double',
5
+ boolean: 'bool',
6
+ json: 'String',
7
+ bytes: 'List<int>',
8
+ blob_ref: 'String',
9
+ crdt: 'List<int>',
10
+ };
11
+ function pascalCase(name) {
12
+ return name
13
+ .split(/[_-]+/)
14
+ .filter((p) => p.length > 0)
15
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
16
+ .join('');
17
+ }
18
+ function camelCase(name) {
19
+ const pascal = pascalCase(name);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ function typeName(name) {
23
+ return name.charAt(0).toUpperCase() + name.slice(1);
24
+ }
25
+ function quote(value) {
26
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\$/g, '\\$')}'`;
27
+ }
28
+ function rowAccessor(column) {
29
+ const key = `row[${quote(column.name)}]`;
30
+ switch (column.type) {
31
+ case 'string':
32
+ case 'json':
33
+ case 'blob_ref':
34
+ return `${key} as String?`;
35
+ case 'integer':
36
+ return `(${key} as num?)?.toInt()`;
37
+ case 'float':
38
+ return `(${key} as num?)?.toDouble()`;
39
+ case 'boolean':
40
+ return `_queryRowBool(${key})`;
41
+ case 'bytes':
42
+ case 'crdt':
43
+ return `_queryRowBytes(${key})`;
44
+ }
45
+ }
46
+ function paramValue(type, name) {
47
+ switch (type) {
48
+ case 'bytes':
49
+ case 'crdt':
50
+ return `_queryBindBytes(${name})`;
51
+ default:
52
+ // String/int/double/bool ride as-is in the params list.
53
+ return name;
54
+ }
55
+ }
56
+ function emitClass(query) {
57
+ const Row = `${typeName(query.name)}Row`;
58
+ const lines = [];
59
+ lines.push(`/// One row of the ${query.name} query (its projection).`);
60
+ lines.push(`class ${Row} {`);
61
+ for (const column of query.columns) {
62
+ const opt = column.nullable ? '?' : '';
63
+ lines.push(` final ${DART_TYPE[column.type]}${opt} ${camelCase(column.name)};`);
64
+ }
65
+ lines.push('');
66
+ const ctorParams = query.columns
67
+ .map((c) => `${c.nullable ? '' : 'required '}this.${camelCase(c.name)}`)
68
+ .join(', ');
69
+ lines.push(` const ${Row}({${ctorParams}});`);
70
+ lines.push('');
71
+ lines.push(` static ${Row}? fromRow(Map<String, Object?> row) {`);
72
+ for (const column of query.columns) {
73
+ if (!column.nullable) {
74
+ const name = camelCase(column.name);
75
+ lines.push(` final ${name} = ${rowAccessor(column)};`);
76
+ lines.push(` if (${name} == null) return null;`);
77
+ }
78
+ }
79
+ const args = query.columns
80
+ .map((c) => {
81
+ const name = camelCase(c.name);
82
+ return c.nullable ? `${name}: ${rowAccessor(c)}` : `${name}: ${name}`;
83
+ })
84
+ .join(', ');
85
+ lines.push(` return ${Row}(${args});`);
86
+ lines.push(' }');
87
+ lines.push('}');
88
+ return lines;
89
+ }
90
+ function emitRunner(query) {
91
+ const Row = `${typeName(query.name)}Row`;
92
+ const Pascal = pascalCase(query.name);
93
+ const lines = [];
94
+ lines.push(`/// Tables the ${query.name} query reads (exact invalidation set).`);
95
+ lines.push(`const List<String> syncular${Pascal}QueryTables = [${query.tables.map(quote).join(', ')}];`);
96
+ lines.push('');
97
+ lines.push(`const String _${query.name}Sql = ${quote(query.positionalSql)};`);
98
+ lines.push('');
99
+ lines.push(`/// Run the ${query.name} named query (SELECT-only).`);
100
+ const args = query.params
101
+ .map((p) => `required ${DART_TYPE[p.type]} ${camelCase(p.name)}`)
102
+ .join(', ');
103
+ const argsClause = query.params.length > 0 ? `, {${args}}` : '';
104
+ lines.push(`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`);
105
+ if (query.params.length > 0) {
106
+ const binds = query.params
107
+ .map((p) => paramValue(p.type, camelCase(p.name)))
108
+ .join(', ');
109
+ lines.push(` final params = <Object?>[${binds}];`);
110
+ lines.push(` return client.query(_${query.name}Sql, params: params).map(${Row}.fromRow).whereType<${Row}>().toList();`);
111
+ }
112
+ else {
113
+ lines.push(` return client.query(_${query.name}Sql).map(${Row}.fromRow).whereType<${Row}>().toList();`);
114
+ }
115
+ lines.push('}');
116
+ return lines;
117
+ }
118
+ export function emitQueriesDartModule(queries, hash, irVersion) {
119
+ const parts = [];
120
+ parts.push([
121
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
122
+ `// irVersion: ${irVersion}`,
123
+ `// irHash: ${hash}`,
124
+ '// ignore_for_file: type=lint',
125
+ '',
126
+ "import 'package:syncular/syncular.dart';",
127
+ ].join('\n'));
128
+ parts.push([
129
+ '/// Lift a SQLite boolean: a real bool, or 0/1 as a number.',
130
+ 'bool? _queryRowBool(Object? value) {',
131
+ ' if (value is bool) return value;',
132
+ ' if (value is num) return value != 0;',
133
+ ' return null;',
134
+ '}',
135
+ '',
136
+ "/// Decode the core's {'\\$bytes': '<hex>'} marshaling (bytes as hex).",
137
+ 'List<int>? _queryRowBytes(Object? value) {',
138
+ ' if (value is! Map) return null;',
139
+ " final hex = value[r'$bytes'];",
140
+ ' if (hex is! String || hex.length % 2 != 0) return null;',
141
+ ' return [',
142
+ ' for (var i = 0; i < hex.length; i += 2)',
143
+ ' int.parse(hex.substring(i, i + 2), radix: 16),',
144
+ ' ];',
145
+ '}',
146
+ '',
147
+ "/// Encode bytes as the core's {'\\$bytes': '<hex>'} marshaling for binding.",
148
+ 'Map<String, Object?> _queryBindBytes(List<int> value) {',
149
+ " final hex = value.map((b) => b.toRadixString(16).padLeft(2, '0')).join();",
150
+ " return {r'$bytes': hex};",
151
+ '}',
152
+ ].join('\n'));
153
+ for (const query of queries) {
154
+ parts.push(emitClass(query).join('\n'));
155
+ }
156
+ for (const query of queries) {
157
+ parts.push(emitRunner(query).join('\n'));
158
+ }
159
+ return `${parts.join('\n\n')}\n`;
160
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalyzedQuery } from './query.js';
2
+ export declare function emitQueriesKotlinModule(queries: readonly AnalyzedQuery[], hash: string, irVersion: number, packageName: string, objectName: string): string;
@@ -0,0 +1,162 @@
1
+ const KOTLIN_TYPE = {
2
+ string: 'String',
3
+ integer: 'Long',
4
+ float: 'Double',
5
+ boolean: 'Boolean',
6
+ json: 'String',
7
+ bytes: 'ByteArray',
8
+ blob_ref: 'String',
9
+ crdt: 'ByteArray',
10
+ };
11
+ function pascalCase(name) {
12
+ return name
13
+ .split(/[_-]+/)
14
+ .filter((p) => p.length > 0)
15
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
16
+ .join('');
17
+ }
18
+ function camelCase(name) {
19
+ const pascal = pascalCase(name);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ function typeName(name) {
23
+ return name.charAt(0).toUpperCase() + name.slice(1);
24
+ }
25
+ function quote(value) {
26
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
27
+ }
28
+ function rowAccessor(column) {
29
+ const key = `row[${quote(column.name)}]`;
30
+ switch (column.type) {
31
+ case 'string':
32
+ case 'json':
33
+ case 'blob_ref':
34
+ return `${key}?.string`;
35
+ case 'integer':
36
+ return `${key}?.number?.toLong()`;
37
+ case 'float':
38
+ return `${key}?.number`;
39
+ case 'boolean':
40
+ return `queryRowBool(${key})`;
41
+ case 'bytes':
42
+ case 'crdt':
43
+ return `queryRowBytes(${key})`;
44
+ }
45
+ }
46
+ function paramValue(type, name) {
47
+ switch (type) {
48
+ case 'integer':
49
+ // JsonValue.of has no Long overload; widen to Double (SQLite numbers).
50
+ return `JsonValue.of(${name}.toDouble())`;
51
+ case 'float':
52
+ return `JsonValue.of(${name})`;
53
+ case 'boolean':
54
+ return `JsonValue.of(${name})`;
55
+ case 'bytes':
56
+ case 'crdt':
57
+ return `queryBindBytes(${name})`;
58
+ default:
59
+ return `JsonValue.of(${name})`;
60
+ }
61
+ }
62
+ function emitDataClass(query) {
63
+ const Row = `${typeName(query.name)}Row`;
64
+ const lines = [];
65
+ lines.push(`/** One row of the ${query.name} query (its projection). */`);
66
+ lines.push(`data class ${Row}(`);
67
+ for (const column of query.columns) {
68
+ const opt = column.nullable ? '?' : '';
69
+ lines.push(` val ${camelCase(column.name)}: ${KOTLIN_TYPE[column.type]}${opt},`);
70
+ }
71
+ lines.push(') {');
72
+ lines.push(' companion object {');
73
+ lines.push(` fun fromRow(row: JsonValue): ${Row}? {`);
74
+ for (const column of query.columns) {
75
+ const name = camelCase(column.name);
76
+ const accessor = rowAccessor(column);
77
+ if (column.nullable) {
78
+ lines.push(` val ${name} = ${accessor}`);
79
+ }
80
+ else {
81
+ lines.push(` val ${name} = ${accessor} ?: return null`);
82
+ }
83
+ }
84
+ const args = query.columns.map((c) => camelCase(c.name)).join(', ');
85
+ lines.push(` return ${Row}(${args})`);
86
+ lines.push(' }');
87
+ lines.push(' }');
88
+ lines.push('}');
89
+ return lines;
90
+ }
91
+ function emitRunner(query) {
92
+ const Row = `${typeName(query.name)}Row`;
93
+ const lines = [];
94
+ lines.push(` val ${query.name}Tables = listOf(${query.tables.map(quote).join(', ')})`);
95
+ lines.push(` private const val ${query.name}Sql = ${quote(query.positionalSql)}`);
96
+ lines.push('');
97
+ lines.push(` /** Run the ${query.name} named query (SELECT-only). */`);
98
+ const args = query.params
99
+ .map((p) => `${camelCase(p.name)}: ${KOTLIN_TYPE[p.type]}`)
100
+ .join(', ');
101
+ const signature = query.params.length > 0
102
+ ? `client: SyncularClient, ${args}`
103
+ : 'client: SyncularClient';
104
+ lines.push(` fun ${query.name}(${signature}): List<${Row}> {`);
105
+ if (query.params.length > 0) {
106
+ const binds = query.params
107
+ .map((p) => paramValue(p.type, camelCase(p.name)))
108
+ .join(', ');
109
+ lines.push(` val params = listOf(${binds})`);
110
+ lines.push(` return client.query(${query.name}Sql, params).mapNotNull { ${Row}.fromRow(it) }`);
111
+ }
112
+ else {
113
+ lines.push(` return client.query(${query.name}Sql).mapNotNull { ${Row}.fromRow(it) }`);
114
+ }
115
+ lines.push(' }');
116
+ return lines;
117
+ }
118
+ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, objectName) {
119
+ const parts = [];
120
+ parts.push([
121
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
122
+ `// irVersion: ${irVersion}`,
123
+ `// irHash: ${hash}`,
124
+ '',
125
+ `package ${packageName}`,
126
+ '',
127
+ 'import dev.syncular.JsonValue',
128
+ 'import dev.syncular.SyncularClient',
129
+ ].join('\n'));
130
+ // Row-decode + bind helpers (package-private; distinct names so they don't
131
+ // clash with the schema file's rowBool/rowBytes in the same package).
132
+ parts.push([
133
+ 'private fun queryRowBool(value: JsonValue?): Boolean? {',
134
+ ' value?.bool?.let { return it }',
135
+ ' value?.number?.let { return it != 0.0 }',
136
+ ' return null',
137
+ '}',
138
+ '',
139
+ 'private fun queryRowBytes(value: JsonValue?): ByteArray? {',
140
+ ' val hex = value?.get("\\$bytes")?.string ?: return null',
141
+ ' if (hex.length % 2 != 0) return null',
142
+ ' return ByteArray(hex.length / 2) { i ->',
143
+ ' hex.substring(i * 2, i * 2 + 2).toInt(16).toByte()',
144
+ ' }',
145
+ '}',
146
+ '',
147
+ 'private fun queryBindBytes(value: ByteArray): JsonValue {',
148
+ ' val hex = value.joinToString("") { "%02x".format(it) }',
149
+ ' return JsonValue.obj("\\$bytes" to JsonValue.of(hex))',
150
+ '}',
151
+ ].join('\n'));
152
+ for (const query of queries) {
153
+ parts.push(emitDataClass(query).join('\n'));
154
+ }
155
+ const objLines = [];
156
+ objLines.push('/** Typed named queries (the sqlc/SQLDelight tier). */');
157
+ objLines.push(`object ${objectName}Queries {`);
158
+ objLines.push(queries.map((q) => emitRunner(q).join('\n')).join('\n\n'));
159
+ objLines.push('}');
160
+ parts.push(objLines.join('\n'));
161
+ return `${parts.join('\n\n')}\n`;
162
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalyzedQuery } from './query.js';
2
+ export declare function emitQueriesSwiftModule(queries: readonly AnalyzedQuery[], hash: string, irVersion: number, enumName: string): string;
@@ -0,0 +1,146 @@
1
+ const SWIFT_TYPE = {
2
+ string: 'String',
3
+ integer: 'Int',
4
+ float: 'Double',
5
+ boolean: 'Bool',
6
+ json: 'String',
7
+ bytes: '[UInt8]',
8
+ blob_ref: 'String',
9
+ crdt: '[UInt8]',
10
+ };
11
+ function pascalCase(name) {
12
+ return name
13
+ .split(/[_-]+/)
14
+ .filter((p) => p.length > 0)
15
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
16
+ .join('');
17
+ }
18
+ function camelCase(name) {
19
+ const pascal = pascalCase(name);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ /** The query function/type name: already camelCase; PascalCase for the type. */
23
+ function typeName(name) {
24
+ return name.charAt(0).toUpperCase() + name.slice(1);
25
+ }
26
+ function quote(value) {
27
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
28
+ }
29
+ function rowAccessor(column) {
30
+ const key = `row[${quote(column.name)}]`;
31
+ switch (column.type) {
32
+ case 'string':
33
+ case 'json':
34
+ case 'blob_ref':
35
+ return `${key}?.stringValue`;
36
+ case 'integer':
37
+ return `${key}?.numberValue.map(Int.init)`;
38
+ case 'float':
39
+ return `${key}?.numberValue`;
40
+ case 'boolean':
41
+ return `SyncularSchemaRow.bool(${key})`;
42
+ case 'bytes':
43
+ case 'crdt':
44
+ return `SyncularSchemaRow.bytes(${key})`;
45
+ }
46
+ }
47
+ function paramValue(type, name) {
48
+ switch (type) {
49
+ case 'integer':
50
+ case 'float':
51
+ return `.number(Double(${name}))`;
52
+ case 'boolean':
53
+ return `.bool(${name})`;
54
+ case 'bytes':
55
+ case 'crdt':
56
+ return `SyncularSchemaQueryBind.bytes(${name})`;
57
+ default:
58
+ return `.string(${name})`;
59
+ }
60
+ }
61
+ function emitRowStruct(query) {
62
+ const Row = `${typeName(query.name)}Row`;
63
+ const lines = [];
64
+ lines.push(`/// One row of the ${query.name} query (its projection).`);
65
+ lines.push(`public struct ${Row}: Sendable, Equatable {`);
66
+ for (const column of query.columns) {
67
+ const opt = column.nullable ? '?' : '';
68
+ lines.push(` public let ${camelCase(column.name)}: ${SWIFT_TYPE[column.type]}${opt}`);
69
+ }
70
+ lines.push('');
71
+ lines.push(' public init?(row: [String: JSONValue]) {');
72
+ for (const column of query.columns) {
73
+ const name = camelCase(column.name);
74
+ const accessor = rowAccessor(column);
75
+ if (column.nullable) {
76
+ lines.push(` self.${name} = ${accessor}`);
77
+ }
78
+ else {
79
+ lines.push(` guard let ${name} = ${accessor} else { return nil }`);
80
+ lines.push(` self.${name} = ${name}`);
81
+ }
82
+ }
83
+ lines.push(' }');
84
+ lines.push('}');
85
+ return lines;
86
+ }
87
+ function emitRunner(query) {
88
+ const Row = `${typeName(query.name)}Row`;
89
+ const lines = [];
90
+ lines.push(` public static let ${query.name}Tables = [${query.tables.map(quote).join(', ')}]`);
91
+ lines.push(` private static let ${query.name}Sql = ${quote(query.positionalSql)}`);
92
+ lines.push('');
93
+ lines.push(` /// Run the ${query.name} named query (SELECT-only).`);
94
+ const args = query.params
95
+ .map((p) => `${camelCase(p.name)}: ${SWIFT_TYPE[p.type]}`)
96
+ .join(', ');
97
+ const signature = query.params.length > 0
98
+ ? `client: SyncularClient, ${args}`
99
+ : 'client: SyncularClient';
100
+ lines.push(` public static func ${query.name}(${signature}) throws -> [${Row}] {`);
101
+ if (query.params.length > 0) {
102
+ const binds = query.params
103
+ .map((p) => paramValue(p.type, camelCase(p.name)))
104
+ .join(', ');
105
+ lines.push(` let params: [JSONValue] = [${binds}]`);
106
+ lines.push(` return try client.query(${query.name}Sql, params: params).compactMap { row in`);
107
+ }
108
+ else {
109
+ lines.push(` return try client.query(${query.name}Sql).compactMap { row in`);
110
+ }
111
+ lines.push(' guard case let .object(fields) = row else { return nil }');
112
+ lines.push(` return ${Row}(row: fields)`);
113
+ lines.push(' }');
114
+ lines.push(' }');
115
+ return lines;
116
+ }
117
+ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
118
+ const parts = [];
119
+ parts.push([
120
+ '// Generated by @syncular/typegen — DO NOT EDIT.',
121
+ `// irVersion: ${irVersion}`,
122
+ `// irHash: ${hash}`,
123
+ '',
124
+ 'import Foundation',
125
+ 'import Syncular',
126
+ ].join('\n'));
127
+ parts.push([
128
+ '/// Param-bind helpers shared by the generated query runners.',
129
+ 'enum SyncularSchemaQueryBind {',
130
+ ' static func bytes(_ value: [UInt8]) -> JSONValue {',
131
+ ' let hex = value.map { String(format: "%02x", $0) }.joined()',
132
+ ' return .object(["$bytes": .string(hex)])',
133
+ ' }',
134
+ '}',
135
+ ].join('\n'));
136
+ for (const query of queries) {
137
+ parts.push(emitRowStruct(query).join('\n'));
138
+ }
139
+ const enumLines = [];
140
+ enumLines.push('/// Typed named queries (the sqlc/SQLDelight tier).');
141
+ enumLines.push(`public enum ${enumName}Queries {`);
142
+ enumLines.push(queries.map((q) => emitRunner(q).join('\n')).join('\n\n'));
143
+ enumLines.push('}');
144
+ parts.push(enumLines.join('\n'));
145
+ return `${parts.join('\n\n')}\n`;
146
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalyzedQuery } from './query.js';
2
+ export declare function emitQueriesModule(queries: readonly AnalyzedQuery[], hash: string, irVersion: number): string;