@syncular/typegen 0.2.1 → 0.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.
- package/README.md +25 -10
- package/dist/cli.js +155 -4
- package/dist/emit-dart.js +3 -2
- package/dist/emit-kotlin.js +3 -2
- package/dist/emit-queries-dart.js +87 -15
- package/dist/emit-queries-kotlin.js +89 -15
- package/dist/emit-queries-swift.js +92 -14
- package/dist/emit-queries.js +131 -22
- package/dist/emit-swift.js +3 -2
- package/dist/emit.d.ts +2 -1
- package/dist/emit.js +13 -27
- package/dist/fmt.d.ts +3 -0
- package/dist/fmt.js +356 -0
- package/dist/generate.d.ts +16 -6
- package/dist/generate.js +34 -9
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lower.d.ts +54 -0
- package/dist/lower.js +283 -0
- package/dist/lsp.d.ts +20 -0
- package/dist/lsp.js +376 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.js +20 -0
- package/dist/naming.d.ts +22 -0
- package/dist/naming.js +240 -0
- package/dist/query-ir.d.ts +14 -0
- package/dist/query-ir.js +69 -0
- package/dist/query.d.ts +102 -6
- package/dist/query.js +126 -34
- package/dist/syql.d.ts +83 -0
- package/dist/syql.js +945 -0
- package/package.json +2 -2
- package/src/cli.ts +155 -4
- package/src/emit-dart.ts +3 -2
- package/src/emit-kotlin.ts +3 -2
- package/src/emit-queries-dart.ts +109 -16
- package/src/emit-queries-kotlin.ts +111 -18
- package/src/emit-queries-swift.ts +106 -17
- package/src/emit-queries.ts +179 -28
- package/src/emit-swift.ts +3 -2
- package/src/emit.ts +18 -28
- package/src/fmt.ts +351 -0
- package/src/generate.ts +54 -11
- package/src/index.ts +6 -0
- package/src/lower.ts +331 -0
- package/src/lsp.ts +445 -0
- package/src/manifest.ts +38 -0
- package/src/naming.ts +271 -0
- package/src/query-ir.ts +82 -0
- package/src/query.ts +241 -34
- package/src/syql.ts +1171 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { snakeToCamel } from './naming.js';
|
|
1
2
|
const KOTLIN_TYPE = {
|
|
2
3
|
string: 'String',
|
|
3
4
|
integer: 'Long',
|
|
@@ -15,9 +16,9 @@ function pascalCase(name) {
|
|
|
15
16
|
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
16
17
|
.join('');
|
|
17
18
|
}
|
|
19
|
+
/** Language-facing field name — the pinned §12 naming map. */
|
|
18
20
|
function camelCase(name) {
|
|
19
|
-
|
|
20
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
21
|
+
return snakeToCamel(name);
|
|
21
22
|
}
|
|
22
23
|
function typeName(name) {
|
|
23
24
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -26,7 +27,7 @@ function quote(value) {
|
|
|
26
27
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
|
|
27
28
|
}
|
|
28
29
|
function rowAccessor(column) {
|
|
29
|
-
const key = `row[${quote(column.
|
|
30
|
+
const key = `row[${quote(column.langName)}]`;
|
|
30
31
|
switch (column.type) {
|
|
31
32
|
case 'string':
|
|
32
33
|
case 'json':
|
|
@@ -59,6 +60,37 @@ function paramValue(type, name) {
|
|
|
59
60
|
return `JsonValue.of(${name})`;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
/** Bind for a §4 OPTIONAL param: `null` rides as JSON null (the §7
|
|
64
|
+
* neutralization guards make it a no-op). */
|
|
65
|
+
function optionalParamValue(type, name) {
|
|
66
|
+
switch (type) {
|
|
67
|
+
case 'integer':
|
|
68
|
+
return `${name}?.let { JsonValue.of(it.toDouble()) } ?: JsonValue.Null`;
|
|
69
|
+
case 'bytes':
|
|
70
|
+
case 'crdt':
|
|
71
|
+
return `${name}?.let { queryBindBytes(it) } ?: JsonValue.Null`;
|
|
72
|
+
default:
|
|
73
|
+
return `${name}?.let { JsonValue.of(it) } ?: JsonValue.Null`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isOptionalParam(query, p) {
|
|
77
|
+
return (p.optional === true ||
|
|
78
|
+
p.flag === true ||
|
|
79
|
+
(query.limit !== undefined && p.name === 'limit'));
|
|
80
|
+
}
|
|
81
|
+
/** Per-query orderBy allowlist enum (column = the checked SQL column). */
|
|
82
|
+
function emitOrderByEnum(query) {
|
|
83
|
+
if (query.orderBy === undefined)
|
|
84
|
+
return [];
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`/** §6 orderBy allowlist for ${query.name} — checked at generate time. */`);
|
|
87
|
+
lines.push(`enum class ${typeName(query.name)}OrderBy(val column: String) {`);
|
|
88
|
+
lines.push(query.orderBy.allowed
|
|
89
|
+
.map((col) => ` ${camelCase(col.langName)}(${quote(col.name)})`)
|
|
90
|
+
.join(',\n') + ';');
|
|
91
|
+
lines.push('}');
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
62
94
|
function emitDataClass(query) {
|
|
63
95
|
const Row = `${typeName(query.name)}Row`;
|
|
64
96
|
const lines = [];
|
|
@@ -66,13 +98,13 @@ function emitDataClass(query) {
|
|
|
66
98
|
lines.push(`data class ${Row}(`);
|
|
67
99
|
for (const column of query.columns) {
|
|
68
100
|
const opt = column.nullable ? '?' : '';
|
|
69
|
-
lines.push(` val ${camelCase(column.
|
|
101
|
+
lines.push(` val ${camelCase(column.langName)}: ${KOTLIN_TYPE[column.type]}${opt},`);
|
|
70
102
|
}
|
|
71
103
|
lines.push(') {');
|
|
72
104
|
lines.push(' companion object {');
|
|
73
105
|
lines.push(` fun fromRow(row: JsonValue): ${Row}? {`);
|
|
74
106
|
for (const column of query.columns) {
|
|
75
|
-
const name = camelCase(column.
|
|
107
|
+
const name = camelCase(column.langName);
|
|
76
108
|
const accessor = rowAccessor(column);
|
|
77
109
|
if (column.nullable) {
|
|
78
110
|
lines.push(` val ${name} = ${accessor}`);
|
|
@@ -81,7 +113,7 @@ function emitDataClass(query) {
|
|
|
81
113
|
lines.push(` val ${name} = ${accessor} ?: return null`);
|
|
82
114
|
}
|
|
83
115
|
}
|
|
84
|
-
const args = query.columns.map((c) => camelCase(c.
|
|
116
|
+
const args = query.columns.map((c) => camelCase(c.langName)).join(', ');
|
|
85
117
|
lines.push(` return ${Row}(${args})`);
|
|
86
118
|
lines.push(' }');
|
|
87
119
|
lines.push(' }');
|
|
@@ -92,25 +124,55 @@ function emitRunner(query) {
|
|
|
92
124
|
const Row = `${typeName(query.name)}Row`;
|
|
93
125
|
const lines = [];
|
|
94
126
|
lines.push(` val ${query.name}Tables = listOf(${query.tables.map(quote).join(', ')})`);
|
|
95
|
-
|
|
127
|
+
if (query.orderBy !== undefined) {
|
|
128
|
+
lines.push(` private const val ${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(` private const val ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
132
|
+
}
|
|
96
133
|
lines.push('');
|
|
97
134
|
lines.push(` /** Run the ${query.name} named query (SELECT-only). */`);
|
|
98
|
-
const args =
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
const args = [];
|
|
136
|
+
for (const p of query.params) {
|
|
137
|
+
const name = camelCase(p.langName);
|
|
138
|
+
if (isOptionalParam(query, p)) {
|
|
139
|
+
args.push(`${name}: ${KOTLIN_TYPE[p.type]}? = null`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
args.push(`${name}: ${KOTLIN_TYPE[p.type]}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (query.orderBy !== undefined) {
|
|
146
|
+
const defaultCase = camelCase(query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
147
|
+
?.langName ?? query.orderBy.defaultColumn);
|
|
148
|
+
args.push(`orderBy: ${typeName(query.name)}OrderBy = ${typeName(query.name)}OrderBy.${defaultCase}`);
|
|
149
|
+
args.push(`dir: SyncularQueryDir = SyncularQueryDir.${query.orderBy.defaultDir.toUpperCase()}`);
|
|
150
|
+
}
|
|
151
|
+
const signature = args.length > 0
|
|
152
|
+
? `client: SyncularClient, ${args.join(', ')}`
|
|
103
153
|
: 'client: SyncularClient';
|
|
104
154
|
lines.push(` fun ${query.name}(${signature}): List<${Row}> {`);
|
|
155
|
+
if (query.orderBy !== undefined) {
|
|
156
|
+
const limitTail = query.positionalLimitTail !== undefined
|
|
157
|
+
? ` + ${quote(query.positionalLimitTail)}`
|
|
158
|
+
: '';
|
|
159
|
+
lines.push(` val sql = ${query.name}SqlBase + " order by " + orderBy.column + " " + dir.sql${limitTail}`);
|
|
160
|
+
}
|
|
161
|
+
const sqlRef = query.orderBy !== undefined ? 'sql' : `${query.name}Sql`;
|
|
105
162
|
if (query.params.length > 0) {
|
|
106
163
|
const binds = query.params
|
|
107
|
-
.map((p) =>
|
|
164
|
+
.map((p) => {
|
|
165
|
+
const name = camelCase(p.langName);
|
|
166
|
+
return isOptionalParam(query, p)
|
|
167
|
+
? optionalParamValue(p.type, name)
|
|
168
|
+
: paramValue(p.type, name);
|
|
169
|
+
})
|
|
108
170
|
.join(', ');
|
|
109
171
|
lines.push(` val params = listOf(${binds})`);
|
|
110
|
-
lines.push(` return client.query(${
|
|
172
|
+
lines.push(` return client.query(${sqlRef}, params).mapNotNull { ${Row}.fromRow(it) }`);
|
|
111
173
|
}
|
|
112
174
|
else {
|
|
113
|
-
lines.push(` return client.query(${
|
|
175
|
+
lines.push(` return client.query(${sqlRef}).mapNotNull { ${Row}.fromRow(it) }`);
|
|
114
176
|
}
|
|
115
177
|
lines.push(' }');
|
|
116
178
|
return lines;
|
|
@@ -149,8 +211,20 @@ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, o
|
|
|
149
211
|
' return JsonValue.obj("\\$bytes" to JsonValue.of(hex))',
|
|
150
212
|
'}',
|
|
151
213
|
].join('\n'));
|
|
214
|
+
if (queries.some((q) => q.orderBy !== undefined)) {
|
|
215
|
+
parts.push([
|
|
216
|
+
'/** §6 orderBy direction (shared by every orderBy-knob query). */',
|
|
217
|
+
'enum class SyncularQueryDir(val sql: String) {',
|
|
218
|
+
' ASC("asc"),',
|
|
219
|
+
' DESC("desc");',
|
|
220
|
+
'}',
|
|
221
|
+
].join('\n'));
|
|
222
|
+
}
|
|
152
223
|
for (const query of queries) {
|
|
153
224
|
parts.push(emitDataClass(query).join('\n'));
|
|
225
|
+
const orderByEnum = emitOrderByEnum(query);
|
|
226
|
+
if (orderByEnum.length > 0)
|
|
227
|
+
parts.push(orderByEnum.join('\n'));
|
|
154
228
|
}
|
|
155
229
|
const objLines = [];
|
|
156
230
|
objLines.push('/** Typed named queries (the sqlc/SQLDelight tier). */');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { snakeToCamel } from './naming.js';
|
|
1
2
|
const SWIFT_TYPE = {
|
|
2
3
|
string: 'String',
|
|
3
4
|
integer: 'Int',
|
|
@@ -15,9 +16,9 @@ function pascalCase(name) {
|
|
|
15
16
|
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
16
17
|
.join('');
|
|
17
18
|
}
|
|
19
|
+
/** Language-facing field name — the pinned §12 naming map. */
|
|
18
20
|
function camelCase(name) {
|
|
19
|
-
|
|
20
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
21
|
+
return snakeToCamel(name);
|
|
21
22
|
}
|
|
22
23
|
/** The query function/type name: already camelCase; PascalCase for the type. */
|
|
23
24
|
function typeName(name) {
|
|
@@ -27,7 +28,7 @@ function quote(value) {
|
|
|
27
28
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
28
29
|
}
|
|
29
30
|
function rowAccessor(column) {
|
|
30
|
-
const key = `row[${quote(column.
|
|
31
|
+
const key = `row[${quote(column.langName)}]`;
|
|
31
32
|
switch (column.type) {
|
|
32
33
|
case 'string':
|
|
33
34
|
case 'json':
|
|
@@ -58,6 +59,28 @@ function paramValue(type, name) {
|
|
|
58
59
|
return `.string(${name})`;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
/** Bind for a §4 OPTIONAL param: `nil` rides as JSON null (the §7
|
|
63
|
+
* neutralization guards make it a no-op). */
|
|
64
|
+
function optionalParamValue(type, name) {
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'integer':
|
|
67
|
+
return `${name}.map { JSONValue.number(Double($0)) } ?? .null`;
|
|
68
|
+
case 'float':
|
|
69
|
+
return `${name}.map { JSONValue.number($0) } ?? .null`;
|
|
70
|
+
case 'boolean':
|
|
71
|
+
return `${name}.map { JSONValue.bool($0) } ?? .null`;
|
|
72
|
+
case 'bytes':
|
|
73
|
+
case 'crdt':
|
|
74
|
+
return `${name}.map { SyncularSchemaQueryBind.bytes($0) } ?? .null`;
|
|
75
|
+
default:
|
|
76
|
+
return `${name}.map { JSONValue.string($0) } ?? .null`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function isOptionalParam(query, p) {
|
|
80
|
+
return (p.optional === true ||
|
|
81
|
+
p.flag === true ||
|
|
82
|
+
(query.limit !== undefined && p.name === 'limit'));
|
|
83
|
+
}
|
|
61
84
|
function emitRowStruct(query) {
|
|
62
85
|
const Row = `${typeName(query.name)}Row`;
|
|
63
86
|
const lines = [];
|
|
@@ -65,12 +88,12 @@ function emitRowStruct(query) {
|
|
|
65
88
|
lines.push(`public struct ${Row}: Sendable, Equatable {`);
|
|
66
89
|
for (const column of query.columns) {
|
|
67
90
|
const opt = column.nullable ? '?' : '';
|
|
68
|
-
lines.push(` public let ${camelCase(column.
|
|
91
|
+
lines.push(` public let ${camelCase(column.langName)}: ${SWIFT_TYPE[column.type]}${opt}`);
|
|
69
92
|
}
|
|
70
93
|
lines.push('');
|
|
71
94
|
lines.push(' public init?(row: [String: JSONValue]) {');
|
|
72
95
|
for (const column of query.columns) {
|
|
73
|
-
const name = camelCase(column.
|
|
96
|
+
const name = camelCase(column.langName);
|
|
74
97
|
const accessor = rowAccessor(column);
|
|
75
98
|
if (column.nullable) {
|
|
76
99
|
lines.push(` self.${name} = ${accessor}`);
|
|
@@ -84,29 +107,72 @@ function emitRowStruct(query) {
|
|
|
84
107
|
lines.push('}');
|
|
85
108
|
return lines;
|
|
86
109
|
}
|
|
110
|
+
/** Per-query orderBy allowlist enum (rawValue = the checked SQL column). */
|
|
111
|
+
function emitOrderByEnum(query) {
|
|
112
|
+
if (query.orderBy === undefined)
|
|
113
|
+
return [];
|
|
114
|
+
const lines = [];
|
|
115
|
+
lines.push(`/// §6 orderBy allowlist for ${query.name} — checked at generate time.`);
|
|
116
|
+
lines.push(`public enum ${typeName(query.name)}OrderBy: String, Sendable {`);
|
|
117
|
+
for (const col of query.orderBy.allowed) {
|
|
118
|
+
lines.push(` case ${camelCase(col.langName)} = ${quote(col.name)}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('}');
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
87
123
|
function emitRunner(query) {
|
|
88
124
|
const Row = `${typeName(query.name)}Row`;
|
|
89
125
|
const lines = [];
|
|
90
126
|
lines.push(` public static let ${query.name}Tables = [${query.tables.map(quote).join(', ')}]`);
|
|
91
|
-
|
|
127
|
+
if (query.orderBy !== undefined) {
|
|
128
|
+
lines.push(` private static let ${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(` private static let ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
132
|
+
}
|
|
92
133
|
lines.push('');
|
|
93
134
|
lines.push(` /// Run the ${query.name} named query (SELECT-only).`);
|
|
94
|
-
const args =
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
const args = [];
|
|
136
|
+
for (const p of query.params) {
|
|
137
|
+
const name = camelCase(p.langName);
|
|
138
|
+
if (isOptionalParam(query, p)) {
|
|
139
|
+
args.push(`${name}: ${SWIFT_TYPE[p.type]}? = nil`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
args.push(`${name}: ${SWIFT_TYPE[p.type]}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (query.orderBy !== undefined) {
|
|
146
|
+
const defaultCase = camelCase(query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
147
|
+
?.langName ?? query.orderBy.defaultColumn);
|
|
148
|
+
args.push(`orderBy: ${typeName(query.name)}OrderBy = .${defaultCase}`);
|
|
149
|
+
args.push(`dir: SyncularQueryDir = .${query.orderBy.defaultDir}`);
|
|
150
|
+
}
|
|
151
|
+
const signature = args.length > 0
|
|
152
|
+
? `client: SyncularClient, ${args.join(', ')}`
|
|
99
153
|
: 'client: SyncularClient';
|
|
100
154
|
lines.push(` public static func ${query.name}(${signature}) throws -> [${Row}] {`);
|
|
155
|
+
const sqlExpr = query.orderBy !== undefined
|
|
156
|
+
? `${query.name}SqlBase + " order by " + orderBy.rawValue + " " + dir.rawValue${query.positionalLimitTail !== undefined ? ` + ${quote(query.positionalLimitTail)}` : ''}`
|
|
157
|
+
: `${query.name}Sql`;
|
|
158
|
+
if (query.orderBy !== undefined) {
|
|
159
|
+
lines.push(` let sql = ${sqlExpr}`);
|
|
160
|
+
}
|
|
161
|
+
const sqlRef = query.orderBy !== undefined ? 'sql' : `${query.name}Sql`;
|
|
101
162
|
if (query.params.length > 0) {
|
|
102
163
|
const binds = query.params
|
|
103
|
-
.map((p) =>
|
|
164
|
+
.map((p) => {
|
|
165
|
+
const name = camelCase(p.langName);
|
|
166
|
+
return isOptionalParam(query, p)
|
|
167
|
+
? optionalParamValue(p.type, name)
|
|
168
|
+
: paramValue(p.type, name);
|
|
169
|
+
})
|
|
104
170
|
.join(', ');
|
|
105
171
|
lines.push(` let params: [JSONValue] = [${binds}]`);
|
|
106
|
-
lines.push(` return try client.query(${
|
|
172
|
+
lines.push(` return try client.query(${sqlRef}, params: params).compactMap { row in`);
|
|
107
173
|
}
|
|
108
174
|
else {
|
|
109
|
-
lines.push(` return try client.query(${
|
|
175
|
+
lines.push(` return try client.query(${sqlRef}).compactMap { row in`);
|
|
110
176
|
}
|
|
111
177
|
lines.push(' guard case let .object(fields) = row else { return nil }');
|
|
112
178
|
lines.push(` return ${Row}(row: fields)`);
|
|
@@ -133,8 +199,20 @@ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
|
|
|
133
199
|
' }',
|
|
134
200
|
'}',
|
|
135
201
|
].join('\n'));
|
|
202
|
+
if (queries.some((q) => q.orderBy !== undefined)) {
|
|
203
|
+
parts.push([
|
|
204
|
+
'/// §6 orderBy direction (shared by every orderBy-knob query).',
|
|
205
|
+
'public enum SyncularQueryDir: String, Sendable {',
|
|
206
|
+
' case asc = "asc"',
|
|
207
|
+
' case desc = "desc"',
|
|
208
|
+
'}',
|
|
209
|
+
].join('\n'));
|
|
210
|
+
}
|
|
136
211
|
for (const query of queries) {
|
|
137
212
|
parts.push(emitRowStruct(query).join('\n'));
|
|
213
|
+
const orderByEnum = emitOrderByEnum(query);
|
|
214
|
+
if (orderByEnum.length > 0)
|
|
215
|
+
parts.push(orderByEnum.join('\n'));
|
|
138
216
|
}
|
|
139
217
|
const enumLines = [];
|
|
140
218
|
enumLines.push('/// Typed named queries (the sqlc/SQLDelight tier).');
|
package/dist/emit-queries.js
CHANGED
|
@@ -20,61 +20,167 @@ function propertyKey(name) {
|
|
|
20
20
|
}
|
|
21
21
|
/** A named-query param value is the SqlValue subset its type maps to. */
|
|
22
22
|
const PARAM_TS_TYPE = TS_TYPE;
|
|
23
|
+
function isOptional(param) {
|
|
24
|
+
return param.optional === true || param.flag === true;
|
|
25
|
+
}
|
|
26
|
+
/** The positional bind expression for one param. `access` is `params` or
|
|
27
|
+
* `params?` depending on whether the params object itself may be absent. */
|
|
28
|
+
function bindExpr(query, param, access) {
|
|
29
|
+
if (query.limit !== undefined && param.name === 'limit') {
|
|
30
|
+
// The default + clamp live IN the SQL (`min(coalesce(?, d), m)`).
|
|
31
|
+
return `${access}.limit ?? null`;
|
|
32
|
+
}
|
|
33
|
+
const key = propertyKey(param.langName);
|
|
34
|
+
return isOptional(param) ? `${access}.${key} ?? null` : `params.${key}`;
|
|
35
|
+
}
|
|
23
36
|
function emitQuery(query) {
|
|
24
37
|
const Row = `${pascalCase(query.name)}Row`;
|
|
25
38
|
const Params = `${pascalCase(query.name)}Params`;
|
|
26
39
|
const lines = [];
|
|
27
|
-
// Row interface
|
|
40
|
+
// Row interface — keyed by the language-facing names, which ARE the
|
|
41
|
+
// runtime result keys (§5 projection lowering aliases them in SQL).
|
|
28
42
|
lines.push(`/** One row of the ${quote(query.name)} query (its projection). */`);
|
|
29
43
|
lines.push(`export interface ${Row} {`);
|
|
30
44
|
for (const column of query.columns) {
|
|
31
|
-
lines.push(` ${propertyKey(column.
|
|
45
|
+
lines.push(` ${propertyKey(column.langName)}: ${TS_TYPE[column.type]}${column.nullable ? ' | null' : ''};`);
|
|
32
46
|
}
|
|
33
47
|
lines.push('}');
|
|
34
48
|
lines.push('');
|
|
35
|
-
|
|
36
|
-
const
|
|
49
|
+
const hasParams = query.params.length > 0 || query.orderBy !== undefined;
|
|
50
|
+
const requiresParams = query.params.some((p) => !isOptional(p) && !(query.limit !== undefined && p.name === 'limit'));
|
|
51
|
+
// Params interface (params and/or knob keys).
|
|
37
52
|
if (hasParams) {
|
|
38
53
|
lines.push(`/** Named parameters for ${quote(query.name)}. */`);
|
|
39
54
|
lines.push(`export interface ${Params} {`);
|
|
40
55
|
for (const param of query.params) {
|
|
41
|
-
|
|
56
|
+
if (query.limit !== undefined && param.name === 'limit')
|
|
57
|
+
continue;
|
|
58
|
+
const opt = isOptional(param);
|
|
59
|
+
lines.push(` ${propertyKey(param.langName)}${opt ? '?' : ''}: ${PARAM_TS_TYPE[param.type]}${opt ? ' | null' : ''};`);
|
|
60
|
+
}
|
|
61
|
+
if (query.orderBy !== undefined) {
|
|
62
|
+
const keys = query.orderBy.allowed
|
|
63
|
+
.map((c) => quote(c.langName))
|
|
64
|
+
.join(' | ');
|
|
65
|
+
lines.push(` /** §6 orderBy knob — a generate-time-checked allowlist. */`);
|
|
66
|
+
lines.push(` orderBy?: ${keys};`);
|
|
67
|
+
lines.push(` dir?: 'asc' | 'desc';`);
|
|
68
|
+
}
|
|
69
|
+
if (query.limit !== undefined) {
|
|
70
|
+
const clamp = query.limit.max !== undefined ? ` (clamped to ${query.limit.max})` : '';
|
|
71
|
+
lines.push(` /** §6 limit knob — binds as a value${clamp}; default ${query.limit.default ?? query.limit.max}. */`);
|
|
72
|
+
lines.push(' limit?: number;');
|
|
42
73
|
}
|
|
43
74
|
lines.push('}');
|
|
44
75
|
lines.push('');
|
|
45
76
|
}
|
|
46
|
-
// Tables dependency set (for
|
|
47
|
-
lines.push(`/** Tables ${quote(query.name)} reads — the exact
|
|
77
|
+
// Tables dependency set (for useRawSql {tables} / useQuery).
|
|
78
|
+
lines.push(`/** Tables ${quote(query.name)} reads — the exact useRawSql \`{tables}\` set. */`);
|
|
48
79
|
lines.push(`export const ${query.name}Tables = [${query.tables.map(quote).join(', ')}] as const;`);
|
|
49
80
|
lines.push('');
|
|
50
|
-
//
|
|
51
|
-
|
|
81
|
+
// SQL constants: the static default statement, plus (orderBy knob) the
|
|
82
|
+
// baked base + column map + compose function.
|
|
52
83
|
const sqlConst = `${query.name}Sql`;
|
|
84
|
+
lines.push(`const ${sqlConst} = ${quote(query.positionalSql)};`);
|
|
85
|
+
const composeFn = `${query.name}ComposeSql`;
|
|
86
|
+
if (query.orderBy !== undefined) {
|
|
87
|
+
const base = `${query.name}SqlBase`;
|
|
88
|
+
const colsConst = `${query.name}OrderColumns`;
|
|
89
|
+
const defaultLang = query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
90
|
+
?.langName ?? query.orderBy.defaultColumn;
|
|
91
|
+
lines.push(`const ${base} = ${quote(query.positionalSqlBase ?? '')};`);
|
|
92
|
+
lines.push(`const ${colsConst} = { ${query.orderBy.allowed
|
|
93
|
+
.map((c) => `${propertyKey(c.langName)}: ${quote(c.name)}`)
|
|
94
|
+
.join(', ')} } as const;`);
|
|
95
|
+
lines.push(`function ${composeFn}(params?: ${Params}): string {`);
|
|
96
|
+
lines.push(` const column = ${colsConst}[params?.orderBy ?? ${quote(defaultLang)}] ?? ${quote(query.orderBy.defaultColumn)};`);
|
|
97
|
+
lines.push(` const dir = (params?.dir ?? ${quote(query.orderBy.defaultDir)}) === 'desc' ? 'desc' : 'asc';`);
|
|
98
|
+
lines.push(` return \`\${${base}} order by \${column} \${dir}${query.positionalLimitTail ?? ''}\`;`);
|
|
99
|
+
lines.push('}');
|
|
100
|
+
}
|
|
101
|
+
// Positional bind list.
|
|
102
|
+
const access = requiresParams ? 'params' : 'params?';
|
|
53
103
|
const positional = hasParams
|
|
54
|
-
? `[${query.params.map((p) =>
|
|
104
|
+
? `[${query.params.map((p) => bindExpr(query, p, access)).join(', ')}]`
|
|
55
105
|
: '[]';
|
|
56
|
-
|
|
106
|
+
// §7 variant backend: one statement per provided-combination, selected by
|
|
107
|
+
// a bitmask over the optional groups (bit i = group i provided/true). The
|
|
108
|
+
// neutralization statement above stays the canonical/default form.
|
|
109
|
+
const selectFn = `${query.name}SelectVariant`;
|
|
110
|
+
const paramArgDecl = requiresParams
|
|
111
|
+
? `params: ${Params}`
|
|
112
|
+
: `params?: ${Params}`;
|
|
113
|
+
if (query.variants !== undefined && query.variantGroups !== undefined) {
|
|
114
|
+
const variantsConst = `${query.name}Variants`;
|
|
115
|
+
lines.push(`const ${variantsConst}: { sql: string; bind: (${paramArgDecl}) => QueryValue[] }[] = [`);
|
|
116
|
+
for (const variant of query.variants) {
|
|
117
|
+
const binds = variant.params
|
|
118
|
+
.map((name) => {
|
|
119
|
+
const param = query.params.find((p) => p.name === name);
|
|
120
|
+
if (param === undefined)
|
|
121
|
+
throw new Error(`unknown param ${name}`);
|
|
122
|
+
return bindExpr(query, param, access);
|
|
123
|
+
})
|
|
124
|
+
.join(', ');
|
|
125
|
+
lines.push(` // [${variant.when.join(' + ') || 'no optional filters'}]`);
|
|
126
|
+
lines.push(` { sql: ${quote(variant.positionalSql)}, bind: (${requiresParams ? 'params' : 'params?'}) => [${binds}] },`);
|
|
127
|
+
}
|
|
128
|
+
lines.push('];');
|
|
129
|
+
lines.push(`function ${selectFn}(${paramArgDecl}): { sql: string; bind: QueryValue[] } {`);
|
|
130
|
+
lines.push(' let mask = 0;');
|
|
131
|
+
query.variantGroups.forEach((group, index) => {
|
|
132
|
+
const langOf = (name) => query.params.find((p) => p.name === name)?.langName ?? name;
|
|
133
|
+
const condition = group.flag
|
|
134
|
+
? `params?.${propertyKey(langOf(group.params[0]))} === true`
|
|
135
|
+
: group.params
|
|
136
|
+
.map((name) => `(params?.${propertyKey(langOf(name))} ?? null) !== null`)
|
|
137
|
+
.join(' && ');
|
|
138
|
+
lines.push(` if (${condition}) mask |= ${1 << index};`);
|
|
139
|
+
});
|
|
140
|
+
lines.push(` const variant = ${variantsConst}[mask] as (typeof ${variantsConst})[number];`);
|
|
141
|
+
lines.push(' return { sql: variant.sql, bind: variant.bind(params) };');
|
|
142
|
+
lines.push('}');
|
|
143
|
+
}
|
|
57
144
|
lines.push('');
|
|
145
|
+
// The runner.
|
|
146
|
+
const paramArg = !hasParams ? '' : paramArgDecl;
|
|
58
147
|
lines.push(`/** Run the ${quote(query.name)} named query (SELECT-only). */`);
|
|
59
148
|
lines.push(`export async function ${query.name}(client: QueryClient${hasParams ? `, ${paramArg}` : ''}): Promise<${Row}[]> {`);
|
|
60
|
-
if (
|
|
61
|
-
lines.push(` const
|
|
149
|
+
if (query.variants !== undefined) {
|
|
150
|
+
lines.push(` const variant = ${selectFn}(params);`);
|
|
151
|
+
lines.push(' const rows = await client.query(variant.sql, variant.bind);');
|
|
62
152
|
}
|
|
63
153
|
else {
|
|
64
|
-
|
|
154
|
+
const sqlExpr = query.orderBy !== undefined ? `${composeFn}(params)` : sqlConst;
|
|
155
|
+
if (hasParams) {
|
|
156
|
+
lines.push(` const rows = await client.query(${sqlExpr}, ${positional});`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lines.push(` const rows = await client.query(${sqlExpr});`);
|
|
160
|
+
}
|
|
65
161
|
}
|
|
66
162
|
lines.push(` return rows as unknown as ${Row}[];`);
|
|
67
163
|
lines.push('}');
|
|
68
164
|
lines.push('');
|
|
69
|
-
// A descriptor for react's `
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
165
|
+
// A descriptor for react's `useQuery` — the SQL (static, composed from
|
|
166
|
+
// the baked allowlist, or variant-selected), the exact table dependency
|
|
167
|
+
// set, and a `bind(params)` → positional array (always paired with the
|
|
168
|
+
// sqlFor selection). Typed by the query's own Row/Params.
|
|
169
|
+
lines.push(`/** Descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\` — sql + tables + row type. */`);
|
|
73
170
|
const paramsTypeArg = hasParams ? Params : 'undefined';
|
|
74
171
|
lines.push(`export const ${query.name}Query: NamedQuery<${Row}, ${paramsTypeArg}> = {`);
|
|
75
172
|
lines.push(` sql: ${sqlConst},`);
|
|
173
|
+
if (query.variants !== undefined) {
|
|
174
|
+
lines.push(` sqlFor: (params: ${Params}) => ${selectFn}(params).sql,`);
|
|
175
|
+
}
|
|
176
|
+
else if (query.orderBy !== undefined) {
|
|
177
|
+
lines.push(` sqlFor: (params: ${Params}) => ${composeFn}(params),`);
|
|
178
|
+
}
|
|
76
179
|
lines.push(` tables: ${query.name}Tables,`);
|
|
77
|
-
if (
|
|
180
|
+
if (query.variants !== undefined) {
|
|
181
|
+
lines.push(` bind: (params: ${Params}) => ${selectFn}(params).bind,`);
|
|
182
|
+
}
|
|
183
|
+
else if (hasParams) {
|
|
78
184
|
lines.push(` bind: (params: ${Params}) => ${positional},`);
|
|
79
185
|
}
|
|
80
186
|
else {
|
|
@@ -112,13 +218,16 @@ export function emitQueriesModule(queries, hash, irVersion) {
|
|
|
112
218
|
'',
|
|
113
219
|
'/** A named-query descriptor — sql + its exact table dependency set + a',
|
|
114
220
|
' * `bind(params)` → positional args. Consumed by',
|
|
115
|
-
" * `@syncular/react`'s `
|
|
116
|
-
' * type; `Params` is `undefined` for a param-less query.
|
|
221
|
+
" * `@syncular/react`'s `useQuery`. `Row` is the projection row",
|
|
222
|
+
' * type; `Params` is `undefined` for a param-less query. `sqlFor`',
|
|
223
|
+
' * (present only with an orderBy knob) composes the statement from a',
|
|
224
|
+
' * generate-time-checked column allowlist. */',
|
|
117
225
|
'export interface NamedQuery<Row, Params = undefined> {',
|
|
118
226
|
' readonly sql: string;',
|
|
119
227
|
' readonly tables: readonly string[];',
|
|
120
228
|
' readonly bind: (params: Params) => readonly QueryValue[];',
|
|
121
|
-
'
|
|
229
|
+
' readonly sqlFor?: (params: Params) => string;',
|
|
230
|
+
' /** Phantom — carries the Row type for `useQuery` inference. */',
|
|
122
231
|
' readonly __row?: Row;',
|
|
123
232
|
'}',
|
|
124
233
|
].join('\n'));
|
package/dist/emit-swift.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { snakeToCamel } from './naming.js';
|
|
1
2
|
/**
|
|
2
3
|
* §2.4 column type → honest Swift type. `json`/`blob_ref` are the raw
|
|
3
4
|
* canonical JSON string (the client's blob API parses blob_ref); `bytes`/
|
|
@@ -27,9 +28,9 @@ function pascalCase(name) {
|
|
|
27
28
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
28
29
|
.join('');
|
|
29
30
|
}
|
|
31
|
+
/** Language-facing field name — the pinned §12 naming map. */
|
|
30
32
|
function camelCase(name) {
|
|
31
|
-
|
|
32
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
33
|
+
return snakeToCamel(name);
|
|
33
34
|
}
|
|
34
35
|
function quote(value) {
|
|
35
36
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
package/dist/emit.d.ts
CHANGED
|
@@ -13,4 +13,5 @@
|
|
|
13
13
|
* byte-exactly.
|
|
14
14
|
*/
|
|
15
15
|
import type { IrDocument } from './ir.js';
|
|
16
|
-
|
|
16
|
+
import { type NamingMode } from './naming.js';
|
|
17
|
+
export declare function emitModule(ir: IrDocument, hash: string, naming?: NamingMode): string;
|