@syncular/typegen 0.0.6-96 → 0.1.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.
- package/README.md +70 -1
- package/dist/app-contract.d.ts +154 -0
- package/dist/app-contract.d.ts.map +1 -0
- package/dist/app-contract.js +250 -0
- package/dist/app-contract.js.map +1 -0
- package/dist/checksums.d.ts +6 -0
- package/dist/checksums.d.ts.map +1 -0
- package/dist/checksums.js +173 -0
- package/dist/checksums.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +88 -0
- package/dist/cli.js.map +1 -0
- package/dist/generate.d.ts +0 -1
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +4 -7
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/introspect-postgres.js +7 -5
- package/dist/introspect-postgres.js.map +1 -1
- package/dist/introspect-sqlite.d.ts.map +1 -1
- package/dist/introspect-sqlite.js +2 -1
- package/dist/introspect-sqlite.js.map +1 -1
- package/dist/introspect.js +2 -2
- package/dist/introspect.js.map +1 -1
- package/dist/map-types.js.map +1 -1
- package/dist/render.d.ts +1 -5
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +2 -21
- package/dist/render.js.map +1 -1
- package/dist/types.d.ts +18 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -6
- package/src/app-contract.ts +531 -0
- package/src/checksums.ts +257 -0
- package/src/cli.ts +104 -0
- package/src/generate.ts +0 -5
- package/src/index.ts +2 -0
- package/src/introspect-postgres.ts +7 -7
- package/src/introspect-sqlite.ts +6 -1
- package/src/render.ts +3 -43
- package/src/types.ts +20 -17
package/dist/types.d.ts
CHANGED
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { DefinedMigrations } from '@syncular/migrations';
|
|
5
5
|
export type TypegenDialect = 'sqlite' | 'postgres';
|
|
6
|
-
export type SyncularImportType = 'scoped' | 'umbrella' | {
|
|
7
|
-
client: string;
|
|
8
|
-
[packageName: string]: string;
|
|
9
|
-
};
|
|
10
6
|
/**
|
|
11
7
|
* Column information for a schema column.
|
|
12
8
|
*/
|
|
@@ -82,15 +78,6 @@ export interface GenerateTypesOptions<DB = unknown> {
|
|
|
82
78
|
output: string;
|
|
83
79
|
/** Database dialect to use for introspection (default: 'sqlite') */
|
|
84
80
|
dialect?: TypegenDialect;
|
|
85
|
-
/** Whether to extend SyncClientDb interface (adds sync infrastructure types) */
|
|
86
|
-
extendsSyncClientDb?: boolean;
|
|
87
|
-
/**
|
|
88
|
-
* Controls how syncular package imports are rendered in generated output.
|
|
89
|
-
* - 'scoped' (default): '@syncular/client'
|
|
90
|
-
* - 'umbrella': 'syncular/client'
|
|
91
|
-
* - object: explicit package mapping (must include `client`)
|
|
92
|
-
*/
|
|
93
|
-
syncularImportType?: SyncularImportType;
|
|
94
81
|
/** Generate versioned interfaces (ClientDbV1, ClientDbV2, etc.) */
|
|
95
82
|
includeVersionHistory?: boolean;
|
|
96
83
|
/** Only generate types for these tables (default: all tables) */
|
|
@@ -114,4 +101,22 @@ export interface GenerateTypesResult {
|
|
|
114
101
|
/** Generated TypeScript code */
|
|
115
102
|
code: string;
|
|
116
103
|
}
|
|
104
|
+
export interface GenerateMigrationChecksumsOptions<DB = unknown> {
|
|
105
|
+
/** Defined migrations from defineMigrations() */
|
|
106
|
+
migrations: DefinedMigrations<DB>;
|
|
107
|
+
/** Output file path for generated checksums */
|
|
108
|
+
output: string;
|
|
109
|
+
/** Database dialect to use for replay (default: 'sqlite') */
|
|
110
|
+
dialect?: TypegenDialect;
|
|
111
|
+
}
|
|
112
|
+
export interface GenerateMigrationChecksumsResult {
|
|
113
|
+
/** Path to the generated file */
|
|
114
|
+
outputPath: string;
|
|
115
|
+
/** Current schema version */
|
|
116
|
+
currentVersion: number;
|
|
117
|
+
/** Number of checksums generated */
|
|
118
|
+
checksumCount: number;
|
|
119
|
+
/** Generated TypeScript code */
|
|
120
|
+
code: string;
|
|
121
|
+
}
|
|
117
122
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "TypeScript type generator for Syncular schemas",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"syncular-typegen": "./src/cli.ts"
|
|
32
|
+
},
|
|
30
33
|
"exports": {
|
|
31
34
|
".": {
|
|
32
35
|
"bun": "./src/index.ts",
|
|
@@ -40,12 +43,12 @@
|
|
|
40
43
|
"test": "bun test --pass-with-no-tests",
|
|
41
44
|
"tsgo": "tsgo --noEmit",
|
|
42
45
|
"build": "tsgo",
|
|
43
|
-
"release": "
|
|
46
|
+
"release": "syncular-publish"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
49
|
"@electric-sql/pglite": "^0.3.15",
|
|
47
|
-
"@syncular/migrations": "
|
|
48
|
-
"better-sqlite3": "^12.
|
|
50
|
+
"@syncular/migrations": "workspace:*",
|
|
51
|
+
"better-sqlite3": "^12.8.0",
|
|
49
52
|
"kysely-bun-sqlite": "^0.4.0",
|
|
50
53
|
"kysely-pglite-dialect": "^1.2.0"
|
|
51
54
|
},
|
|
@@ -53,12 +56,19 @@
|
|
|
53
56
|
"kysely": "^0.28.0"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
|
-
"@syncular/config": "
|
|
59
|
+
"@syncular/config": "workspace:*",
|
|
57
60
|
"@types/better-sqlite3": "^7.6.13",
|
|
58
61
|
"kysely": "*"
|
|
59
62
|
},
|
|
60
63
|
"files": [
|
|
61
64
|
"dist",
|
|
62
|
-
"src"
|
|
65
|
+
"src",
|
|
66
|
+
"!src/**/*.test.ts",
|
|
67
|
+
"!src/**/*.test.tsx",
|
|
68
|
+
"!src/**/__tests__/**",
|
|
69
|
+
"!dist/**/*.test.*",
|
|
70
|
+
"!dist/**/__tests__/**",
|
|
71
|
+
"!test/**",
|
|
72
|
+
"!tests/**"
|
|
63
73
|
]
|
|
64
74
|
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import type { DefinedMigrations } from '@syncular/migrations';
|
|
5
|
+
import { introspectCurrentSchema } from './introspect';
|
|
6
|
+
import type { TableSchema, TypegenDialect } from './types';
|
|
7
|
+
|
|
8
|
+
export type SyncularScopeSource = 'actorId' | 'projectId';
|
|
9
|
+
export type SyncularCrdtYjsKind = 'text' | 'xml-fragment' | 'prosemirror';
|
|
10
|
+
export type SyncularCrdtYjsSyncMode = 'server-merge' | 'encrypted-update-log';
|
|
11
|
+
|
|
12
|
+
export interface SyncularScopeDefinition {
|
|
13
|
+
name?: string;
|
|
14
|
+
column: string;
|
|
15
|
+
source: SyncularScopeSource;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SyncularCrdtYjsFieldDefinition {
|
|
20
|
+
field?: string;
|
|
21
|
+
stateColumn: string;
|
|
22
|
+
containerKey?: string;
|
|
23
|
+
rowIdField?: string;
|
|
24
|
+
kind?: SyncularCrdtYjsKind;
|
|
25
|
+
syncMode?: SyncularCrdtYjsSyncMode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SyncularEncryptedFieldDefinition {
|
|
29
|
+
field: string;
|
|
30
|
+
scope?: string;
|
|
31
|
+
rowIdField?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SyncedTableDefinition {
|
|
35
|
+
table: string;
|
|
36
|
+
subscriptionId?: string;
|
|
37
|
+
subscriptionParams?: Record<string, unknown>;
|
|
38
|
+
scopes?: readonly SyncularScopeDefinition[];
|
|
39
|
+
serverVersion: string;
|
|
40
|
+
blobColumns?: readonly string[];
|
|
41
|
+
crdt?: Record<string, SyncularCrdtYjsFieldDefinition>;
|
|
42
|
+
crdtYjsFields?: readonly SyncularCrdtYjsFieldDefinition[];
|
|
43
|
+
encryptedFields?: readonly SyncularEncryptedFieldDefinition[];
|
|
44
|
+
softDelete?: string;
|
|
45
|
+
sqliteWithoutRowid?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SyncularCountByReadModelDefinition {
|
|
49
|
+
name: string;
|
|
50
|
+
kind: 'countBy';
|
|
51
|
+
sourceTable: string;
|
|
52
|
+
outputTable: string;
|
|
53
|
+
dimensions: readonly string[];
|
|
54
|
+
countColumn?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SyncularLocalReadModelDefinition =
|
|
58
|
+
SyncularCountByReadModelDefinition;
|
|
59
|
+
|
|
60
|
+
export interface SyncularClientSchemaSupportDefinition {
|
|
61
|
+
minSupported?: number;
|
|
62
|
+
supported?: readonly number[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SyncularCodegenPathsDefinition {
|
|
66
|
+
schemaOutputPath?: string;
|
|
67
|
+
typescriptOutputPath?: string;
|
|
68
|
+
typescriptServerOutputPath?: string;
|
|
69
|
+
typescriptRuntimeImportPath?: string;
|
|
70
|
+
rustRuntimeCratePath?: string;
|
|
71
|
+
nativeSwiftOutputPath?: string;
|
|
72
|
+
nativeKotlinOutputPath?: string;
|
|
73
|
+
nativeAndroidKotlinOutputPath?: string;
|
|
74
|
+
nativeAndroidKotlinPackage?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DefineSyncularClientOptions<
|
|
78
|
+
Tables extends Record<string, SyncedTableDefinition>,
|
|
79
|
+
> extends SyncularCodegenPathsDefinition {
|
|
80
|
+
migrations?: unknown;
|
|
81
|
+
tables: Tables;
|
|
82
|
+
localOnlyTables?: readonly string[];
|
|
83
|
+
localReadModels?: readonly SyncularLocalReadModelDefinition[];
|
|
84
|
+
clientSchemaSupport?: SyncularClientSchemaSupportDefinition;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SyncularClientContract<
|
|
88
|
+
Tables extends Record<string, SyncedTableDefinition> = Record<
|
|
89
|
+
string,
|
|
90
|
+
SyncedTableDefinition
|
|
91
|
+
>,
|
|
92
|
+
> extends DefineSyncularClientOptions<Tables> {
|
|
93
|
+
readonly kind: 'syncular-client-contract';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SyncularCodegenScopeConfig {
|
|
97
|
+
name?: string;
|
|
98
|
+
column: string;
|
|
99
|
+
source: SyncularScopeSource;
|
|
100
|
+
required?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SyncularCodegenCrdtYjsFieldConfig {
|
|
104
|
+
field: string;
|
|
105
|
+
stateColumn: string;
|
|
106
|
+
containerKey?: string;
|
|
107
|
+
rowIdField?: string;
|
|
108
|
+
kind?: SyncularCrdtYjsKind;
|
|
109
|
+
syncMode?: SyncularCrdtYjsSyncMode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SyncularCodegenEncryptedFieldConfig {
|
|
113
|
+
field: string;
|
|
114
|
+
scope?: string;
|
|
115
|
+
rowIdField?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface SyncularCodegenTableConfig {
|
|
119
|
+
subscriptionId?: string;
|
|
120
|
+
subscriptionParams?: Record<string, unknown>;
|
|
121
|
+
scopes?: SyncularCodegenScopeConfig[];
|
|
122
|
+
serverVersionColumn: string;
|
|
123
|
+
blobColumns?: string[];
|
|
124
|
+
crdtYjsFields?: SyncularCodegenCrdtYjsFieldConfig[];
|
|
125
|
+
encryptedFields?: SyncularCodegenEncryptedFieldConfig[];
|
|
126
|
+
softDeleteColumn?: string;
|
|
127
|
+
sqliteWithoutRowid?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface SyncularCodegenLocalReadModelConfig {
|
|
131
|
+
name: string;
|
|
132
|
+
kind: 'countBy';
|
|
133
|
+
sourceTable: string;
|
|
134
|
+
outputTable: string;
|
|
135
|
+
dimensions: string[];
|
|
136
|
+
countColumn: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface SyncularCodegenConfig extends SyncularCodegenPathsDefinition {
|
|
140
|
+
tables: Record<string, SyncularCodegenTableConfig>;
|
|
141
|
+
localOnlyTables?: string[];
|
|
142
|
+
localReadModels?: SyncularCodegenLocalReadModelConfig[];
|
|
143
|
+
clientSchemaSupport?: {
|
|
144
|
+
minSupported?: number;
|
|
145
|
+
supported?: number[];
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ScaffoldSyncularClientContractOptions<DB = unknown>
|
|
150
|
+
extends SyncularCodegenPathsDefinition {
|
|
151
|
+
migrations: DefinedMigrations<DB>;
|
|
152
|
+
dialect?: TypegenDialect;
|
|
153
|
+
tables?: readonly string[];
|
|
154
|
+
scopes?: Record<string, readonly SyncularScopeDefinition[]>;
|
|
155
|
+
serverVersionColumn?:
|
|
156
|
+
| string
|
|
157
|
+
| Record<string, string>
|
|
158
|
+
| ((table: TableSchema) => string);
|
|
159
|
+
subscriptionId?:
|
|
160
|
+
| string
|
|
161
|
+
| Record<string, string>
|
|
162
|
+
| ((table: string) => string);
|
|
163
|
+
sqliteWithoutRowid?:
|
|
164
|
+
| boolean
|
|
165
|
+
| Record<string, boolean>
|
|
166
|
+
| ((table: TableSchema) => boolean);
|
|
167
|
+
clientSchemaSupport?: SyncularClientSchemaSupportDefinition;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function defineSyncularClient<
|
|
171
|
+
Tables extends Record<string, SyncedTableDefinition>,
|
|
172
|
+
>(
|
|
173
|
+
options: DefineSyncularClientOptions<Tables>
|
|
174
|
+
): SyncularClientContract<Tables> {
|
|
175
|
+
return {
|
|
176
|
+
...options,
|
|
177
|
+
kind: 'syncular-client-contract',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isSyncularClientContract(
|
|
182
|
+
value: unknown
|
|
183
|
+
): value is SyncularClientContract {
|
|
184
|
+
return (
|
|
185
|
+
typeof value === 'object' &&
|
|
186
|
+
value !== null &&
|
|
187
|
+
(value as { kind?: unknown }).kind === 'syncular-client-contract' &&
|
|
188
|
+
typeof (value as { tables?: unknown }).tables === 'object' &&
|
|
189
|
+
(value as { tables?: unknown }).tables !== null
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function syncedTable(
|
|
194
|
+
options: SyncedTableDefinition
|
|
195
|
+
): SyncedTableDefinition {
|
|
196
|
+
return { ...options };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function scope(
|
|
200
|
+
name: string,
|
|
201
|
+
options: {
|
|
202
|
+
column?: string;
|
|
203
|
+
source: SyncularScopeSource;
|
|
204
|
+
required?: boolean;
|
|
205
|
+
}
|
|
206
|
+
): SyncularScopeDefinition {
|
|
207
|
+
return {
|
|
208
|
+
name,
|
|
209
|
+
column: options.column ?? name,
|
|
210
|
+
source: options.source,
|
|
211
|
+
required: options.required,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function yjsText(
|
|
216
|
+
options: Omit<SyncularCrdtYjsFieldDefinition, 'kind'>
|
|
217
|
+
): SyncularCrdtYjsFieldDefinition {
|
|
218
|
+
return { ...options, kind: 'text' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function encryptedField(
|
|
222
|
+
field: string,
|
|
223
|
+
options: Omit<SyncularEncryptedFieldDefinition, 'field'> = {}
|
|
224
|
+
): SyncularEncryptedFieldDefinition {
|
|
225
|
+
return { field, ...options };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function countByReadModel(
|
|
229
|
+
options: Omit<SyncularCountByReadModelDefinition, 'kind'>
|
|
230
|
+
): SyncularCountByReadModelDefinition {
|
|
231
|
+
return { ...options, kind: 'countBy' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function toSyncularCodegenConfig(
|
|
235
|
+
contract: SyncularClientContract
|
|
236
|
+
): SyncularCodegenConfig {
|
|
237
|
+
const config: SyncularCodegenConfig = {
|
|
238
|
+
tables: Object.fromEntries(
|
|
239
|
+
Object.values(contract.tables).map((table) => [
|
|
240
|
+
table.table,
|
|
241
|
+
toCodegenTable(table),
|
|
242
|
+
])
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
for (const key of CODEGEN_PATH_KEYS) {
|
|
247
|
+
const value = contract[key];
|
|
248
|
+
if (value !== undefined) {
|
|
249
|
+
config[key] = value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (contract.localOnlyTables && contract.localOnlyTables.length > 0) {
|
|
254
|
+
config.localOnlyTables = [...contract.localOnlyTables];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (contract.localReadModels && contract.localReadModels.length > 0) {
|
|
258
|
+
config.localReadModels = contract.localReadModels.map((model) => ({
|
|
259
|
+
name: model.name,
|
|
260
|
+
kind: model.kind,
|
|
261
|
+
sourceTable: model.sourceTable,
|
|
262
|
+
outputTable: model.outputTable,
|
|
263
|
+
dimensions: [...model.dimensions],
|
|
264
|
+
countColumn: model.countColumn ?? 'row_count',
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (contract.clientSchemaSupport) {
|
|
269
|
+
config.clientSchemaSupport = {
|
|
270
|
+
...(contract.clientSchemaSupport.minSupported !== undefined
|
|
271
|
+
? { minSupported: contract.clientSchemaSupport.minSupported }
|
|
272
|
+
: {}),
|
|
273
|
+
...(contract.clientSchemaSupport.supported !== undefined
|
|
274
|
+
? { supported: [...contract.clientSchemaSupport.supported] }
|
|
275
|
+
: {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return config;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function toSyncularCodegenJson(
|
|
283
|
+
contract: SyncularClientContract,
|
|
284
|
+
space = 2
|
|
285
|
+
): string {
|
|
286
|
+
return `${JSON.stringify(toSyncularCodegenConfig(contract), null, space)}\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function writeSyncularCodegenJson(
|
|
290
|
+
contract: SyncularClientContract,
|
|
291
|
+
outputPath: string | URL = 'generated/syncular.codegen.json',
|
|
292
|
+
space = 2
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
if (typeof outputPath === 'string') {
|
|
295
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
296
|
+
} else if (outputPath.protocol === 'file:') {
|
|
297
|
+
await mkdir(dirname(fileURLToPath(outputPath)), { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
await writeFile(outputPath, toSyncularCodegenJson(contract, space));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface LoadSyncularClientContractOptions {
|
|
303
|
+
modulePath: string | URL;
|
|
304
|
+
exportName?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function loadSyncularClientContract(
|
|
308
|
+
options: LoadSyncularClientContractOptions
|
|
309
|
+
): Promise<SyncularClientContract> {
|
|
310
|
+
const exportName = options.exportName ?? 'app';
|
|
311
|
+
const module = (await import(
|
|
312
|
+
syncularContractModuleSpecifier(options.modulePath)
|
|
313
|
+
)) as Record<string, unknown>;
|
|
314
|
+
const contract = module[exportName];
|
|
315
|
+
if (!isSyncularClientContract(contract)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Syncular app contract module must export ${exportName} from defineSyncularClient(...)`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return contract;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface WriteSyncularCodegenJsonFromModuleOptions
|
|
324
|
+
extends LoadSyncularClientContractOptions {
|
|
325
|
+
outputPath?: string | URL;
|
|
326
|
+
space?: number;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function writeSyncularCodegenJsonFromModule(
|
|
330
|
+
options: WriteSyncularCodegenJsonFromModuleOptions
|
|
331
|
+
): Promise<SyncularClientContract> {
|
|
332
|
+
const contract = await loadSyncularClientContract(options);
|
|
333
|
+
await writeSyncularCodegenJson(
|
|
334
|
+
contract,
|
|
335
|
+
options.outputPath ?? 'generated/syncular.codegen.json',
|
|
336
|
+
options.space
|
|
337
|
+
);
|
|
338
|
+
return contract;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function scaffoldSyncularClientContract<DB = unknown>(
|
|
342
|
+
options: ScaffoldSyncularClientContractOptions<DB>
|
|
343
|
+
): Promise<SyncularClientContract<Record<string, SyncedTableDefinition>>> {
|
|
344
|
+
const schema = await introspectCurrentSchema(
|
|
345
|
+
options.migrations,
|
|
346
|
+
options.dialect ?? 'sqlite',
|
|
347
|
+
options.tables ? [...options.tables] : undefined
|
|
348
|
+
);
|
|
349
|
+
const tables = Object.fromEntries(
|
|
350
|
+
schema.tables.map((table) => {
|
|
351
|
+
const serverVersion = resolveServerVersionColumn(
|
|
352
|
+
table,
|
|
353
|
+
options.serverVersionColumn
|
|
354
|
+
);
|
|
355
|
+
assertColumnExists(table, serverVersion, 'server version');
|
|
356
|
+
const synced = syncedTable({
|
|
357
|
+
table: table.name,
|
|
358
|
+
subscriptionId: resolveStringOption(
|
|
359
|
+
table.name,
|
|
360
|
+
options.subscriptionId,
|
|
361
|
+
`sub-${table.name}`
|
|
362
|
+
),
|
|
363
|
+
serverVersion,
|
|
364
|
+
scopes: options.scopes?.[table.name] ?? [],
|
|
365
|
+
sqliteWithoutRowid: resolveBooleanOption(
|
|
366
|
+
table,
|
|
367
|
+
options.sqliteWithoutRowid
|
|
368
|
+
),
|
|
369
|
+
});
|
|
370
|
+
return [table.name, synced];
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return defineSyncularClient({
|
|
375
|
+
...pickCodegenPathOptions(options),
|
|
376
|
+
clientSchemaSupport: options.clientSchemaSupport,
|
|
377
|
+
tables,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const CODEGEN_PATH_KEYS = [
|
|
382
|
+
'schemaOutputPath',
|
|
383
|
+
'typescriptOutputPath',
|
|
384
|
+
'typescriptServerOutputPath',
|
|
385
|
+
'typescriptRuntimeImportPath',
|
|
386
|
+
'rustRuntimeCratePath',
|
|
387
|
+
'nativeSwiftOutputPath',
|
|
388
|
+
'nativeKotlinOutputPath',
|
|
389
|
+
'nativeAndroidKotlinOutputPath',
|
|
390
|
+
'nativeAndroidKotlinPackage',
|
|
391
|
+
] as const satisfies readonly (keyof SyncularCodegenPathsDefinition)[];
|
|
392
|
+
|
|
393
|
+
function toCodegenTable(
|
|
394
|
+
table: SyncedTableDefinition
|
|
395
|
+
): SyncularCodegenTableConfig {
|
|
396
|
+
const config: SyncularCodegenTableConfig = {
|
|
397
|
+
serverVersionColumn: table.serverVersion,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (table.subscriptionId !== undefined) {
|
|
401
|
+
config.subscriptionId = table.subscriptionId;
|
|
402
|
+
}
|
|
403
|
+
if (table.subscriptionParams !== undefined) {
|
|
404
|
+
config.subscriptionParams = table.subscriptionParams;
|
|
405
|
+
}
|
|
406
|
+
if (table.scopes && table.scopes.length > 0) {
|
|
407
|
+
config.scopes = table.scopes.map((item) => ({
|
|
408
|
+
...(item.name !== undefined ? { name: item.name } : {}),
|
|
409
|
+
column: item.column,
|
|
410
|
+
source: item.source,
|
|
411
|
+
...(item.required !== undefined ? { required: item.required } : {}),
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
if (table.blobColumns && table.blobColumns.length > 0) {
|
|
415
|
+
config.blobColumns = [...table.blobColumns];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const crdtFields = [
|
|
419
|
+
...Object.entries(table.crdt ?? {}).map(([field, definition]) => ({
|
|
420
|
+
field,
|
|
421
|
+
...definition,
|
|
422
|
+
})),
|
|
423
|
+
...(table.crdtYjsFields ?? []),
|
|
424
|
+
];
|
|
425
|
+
if (crdtFields.length > 0) {
|
|
426
|
+
config.crdtYjsFields = crdtFields.map((field) => ({
|
|
427
|
+
field: field.field ?? '',
|
|
428
|
+
stateColumn: field.stateColumn,
|
|
429
|
+
...(field.containerKey !== undefined
|
|
430
|
+
? { containerKey: field.containerKey }
|
|
431
|
+
: {}),
|
|
432
|
+
...(field.rowIdField !== undefined
|
|
433
|
+
? { rowIdField: field.rowIdField }
|
|
434
|
+
: {}),
|
|
435
|
+
...(field.kind !== undefined ? { kind: field.kind } : {}),
|
|
436
|
+
...(field.syncMode !== undefined ? { syncMode: field.syncMode } : {}),
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
if (table.encryptedFields && table.encryptedFields.length > 0) {
|
|
440
|
+
config.encryptedFields = table.encryptedFields.map((field) => ({
|
|
441
|
+
field: field.field,
|
|
442
|
+
...(field.scope !== undefined ? { scope: field.scope } : {}),
|
|
443
|
+
...(field.rowIdField !== undefined
|
|
444
|
+
? { rowIdField: field.rowIdField }
|
|
445
|
+
: {}),
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
if (table.softDelete !== undefined) {
|
|
449
|
+
config.softDeleteColumn = table.softDelete;
|
|
450
|
+
}
|
|
451
|
+
if (table.sqliteWithoutRowid !== undefined) {
|
|
452
|
+
config.sqliteWithoutRowid = table.sqliteWithoutRowid;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return config;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function pickCodegenPathOptions(
|
|
459
|
+
options: SyncularCodegenPathsDefinition
|
|
460
|
+
): SyncularCodegenPathsDefinition {
|
|
461
|
+
const picked: SyncularCodegenPathsDefinition = {};
|
|
462
|
+
for (const key of CODEGEN_PATH_KEYS) {
|
|
463
|
+
const value = options[key];
|
|
464
|
+
if (value !== undefined) {
|
|
465
|
+
picked[key] = value;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return picked;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function syncularContractModuleSpecifier(modulePath: string | URL): string {
|
|
472
|
+
if (modulePath instanceof URL) return modulePath.href;
|
|
473
|
+
try {
|
|
474
|
+
const parsed = new URL(modulePath);
|
|
475
|
+
if (parsed.protocol === 'file:') return parsed.href;
|
|
476
|
+
} catch {
|
|
477
|
+
// Treat non-URL strings as filesystem paths or package specifiers below.
|
|
478
|
+
}
|
|
479
|
+
if (syncularLooksLikeLocalModulePath(modulePath)) {
|
|
480
|
+
return pathToFileURL(resolve(modulePath)).href;
|
|
481
|
+
}
|
|
482
|
+
return modulePath;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function syncularLooksLikeLocalModulePath(modulePath: string): boolean {
|
|
486
|
+
return (
|
|
487
|
+
modulePath.startsWith('.') ||
|
|
488
|
+
modulePath.startsWith('/') ||
|
|
489
|
+
(!modulePath.startsWith('@') && modulePath.includes('/')) ||
|
|
490
|
+
/\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$/.test(modulePath)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function resolveServerVersionColumn(
|
|
495
|
+
table: TableSchema,
|
|
496
|
+
option: ScaffoldSyncularClientContractOptions['serverVersionColumn']
|
|
497
|
+
): string {
|
|
498
|
+
if (typeof option === 'function') return option(table);
|
|
499
|
+
if (typeof option === 'string') return option;
|
|
500
|
+
return option?.[table.name] ?? 'server_version';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function resolveStringOption(
|
|
504
|
+
table: string,
|
|
505
|
+
option: ScaffoldSyncularClientContractOptions['subscriptionId'],
|
|
506
|
+
fallback: string
|
|
507
|
+
): string {
|
|
508
|
+
if (typeof option === 'function') return option(table);
|
|
509
|
+
if (typeof option === 'string') return option;
|
|
510
|
+
return option?.[table] ?? fallback;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function resolveBooleanOption(
|
|
514
|
+
table: TableSchema,
|
|
515
|
+
option: ScaffoldSyncularClientContractOptions['sqliteWithoutRowid']
|
|
516
|
+
): boolean | undefined {
|
|
517
|
+
if (typeof option === 'function') return option(table);
|
|
518
|
+
if (typeof option === 'boolean') return option;
|
|
519
|
+
return option?.[table.name];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function assertColumnExists(
|
|
523
|
+
table: TableSchema,
|
|
524
|
+
column: string,
|
|
525
|
+
label: string
|
|
526
|
+
): void {
|
|
527
|
+
if (table.columns.some((candidate) => candidate.name === column)) return;
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Cannot scaffold Syncular table ${table.name}: ${label} column ${column} does not exist`
|
|
530
|
+
);
|
|
531
|
+
}
|