@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.
- package/README.md +436 -94
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +159 -73
- package/dist/emit-dart.d.ts +17 -0
- package/dist/emit-dart.js +201 -0
- package/dist/emit-kotlin.d.ts +16 -0
- package/dist/emit-kotlin.js +201 -0
- package/dist/emit-queries-dart.d.ts +2 -0
- package/dist/emit-queries-dart.js +160 -0
- package/dist/emit-queries-kotlin.d.ts +2 -0
- package/dist/emit-queries-kotlin.js +162 -0
- package/dist/emit-queries-swift.d.ts +2 -0
- package/dist/emit-queries-swift.js +146 -0
- package/dist/emit-queries.d.ts +2 -0
- package/dist/emit-queries.js +129 -0
- package/dist/emit-swift.d.ts +18 -0
- package/dist/emit-swift.js +235 -0
- package/dist/emit.d.ts +16 -0
- package/dist/emit.js +191 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/generate.d.ts +53 -18
- package/dist/generate.js +391 -77
- package/dist/index.d.ts +17 -14
- package/dist/index.js +16 -13
- package/dist/init.d.ts +12 -0
- package/dist/init.js +85 -0
- package/dist/ir.d.ts +90 -0
- package/dist/ir.js +94 -0
- package/dist/manifest.d.ts +76 -0
- package/dist/manifest.js +303 -0
- package/dist/query.d.ts +96 -0
- package/dist/query.js +719 -0
- package/dist/sql.d.ts +13 -0
- package/dist/sql.js +440 -0
- package/package.json +14 -34
- package/src/cli.ts +161 -82
- package/src/emit-dart.ts +245 -0
- package/src/emit-kotlin.ts +257 -0
- package/src/emit-queries-dart.ts +203 -0
- package/src/emit-queries-kotlin.ts +209 -0
- package/src/emit-queries-swift.ts +198 -0
- package/src/emit-queries.ts +183 -0
- package/src/emit-swift.ts +295 -0
- package/src/emit.ts +239 -0
- package/src/errors.ts +11 -0
- package/src/generate.ts +574 -105
- package/src/index.ts +16 -13
- package/src/init.ts +100 -0
- package/src/ir.ts +197 -0
- package/src/manifest.ts +445 -0
- package/src/query.ts +918 -0
- package/src/sql.ts +533 -0
- package/dist/app-contract.d.ts +0 -154
- package/dist/app-contract.d.ts.map +0 -1
- package/dist/app-contract.js +0 -250
- package/dist/app-contract.js.map +0 -1
- package/dist/checksums.d.ts +0 -6
- package/dist/checksums.d.ts.map +0 -1
- package/dist/checksums.js +0 -173
- package/dist/checksums.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/introspect-postgres.d.ts +0 -8
- package/dist/introspect-postgres.d.ts.map +0 -1
- package/dist/introspect-postgres.js +0 -94
- package/dist/introspect-postgres.js.map +0 -1
- package/dist/introspect-sqlite.d.ts +0 -10
- package/dist/introspect-sqlite.d.ts.map +0 -1
- package/dist/introspect-sqlite.js +0 -97
- package/dist/introspect-sqlite.js.map +0 -1
- package/dist/introspect.d.ts +0 -8
- package/dist/introspect.d.ts.map +0 -1
- package/dist/introspect.js +0 -18
- package/dist/introspect.js.map +0 -1
- package/dist/map-types.d.ts +0 -20
- package/dist/map-types.d.ts.map +0 -1
- package/dist/map-types.js +0 -151
- package/dist/map-types.js.map +0 -1
- package/dist/render.d.ts +0 -23
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -140
- package/dist/render.js.map +0 -1
- package/dist/types.d.ts +0 -122
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/app-contract.ts +0 -531
- package/src/checksums.ts +0 -257
- package/src/introspect-postgres.ts +0 -149
- package/src/introspect-sqlite.ts +0 -156
- package/src/introspect.ts +0 -36
- package/src/map-types.ts +0 -189
- package/src/render.ts +0 -196
- package/src/types.ts +0 -137
package/src/index.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @syncular/typegen
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 1. Applying migrations to an in-memory database (SQLite or PostgreSQL)
|
|
6
|
-
* 2. Introspecting the resulting schema
|
|
7
|
-
* 3. Generating TypeScript interfaces
|
|
2
|
+
* @syncular/typegen — SQL migrations + syncular.json → neutral schema
|
|
3
|
+
* IR (JSON) → generated TS module (REVISE B5). Dependency-free; the CLI
|
|
4
|
+
* lives in `src/cli.ts` (bin `syncular`).
|
|
8
5
|
*/
|
|
9
|
-
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './
|
|
6
|
+
export * from './emit';
|
|
7
|
+
export * from './emit-dart';
|
|
8
|
+
export * from './emit-kotlin';
|
|
9
|
+
export * from './emit-queries';
|
|
10
|
+
export * from './emit-queries-dart';
|
|
11
|
+
export * from './emit-queries-kotlin';
|
|
12
|
+
export * from './emit-queries-swift';
|
|
13
|
+
export * from './emit-swift';
|
|
14
|
+
export * from './errors';
|
|
12
15
|
export * from './generate';
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
16
|
-
export * from './
|
|
16
|
+
export * from './ir';
|
|
17
|
+
export * from './manifest';
|
|
18
|
+
export * from './query';
|
|
19
|
+
export * from './sql';
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncular init` — drop a starter `syncular.json` + `migrations/0001_initial`
|
|
3
|
+
* into an existing project (the "add syncular to my app" path). Writes nothing
|
|
4
|
+
* that would clobber an existing manifest or migration; fails loud instead.
|
|
5
|
+
*
|
|
6
|
+
* The starter mirrors the `create-app` minimal template's schema shape so the
|
|
7
|
+
* two on-ramps agree. Kept dependency-free and pure of process concerns.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
import { MANIFEST_FILENAME } from './manifest';
|
|
12
|
+
|
|
13
|
+
const STARTER_MANIFEST = `{
|
|
14
|
+
"manifestVersion": 1,
|
|
15
|
+
"migrations": "./migrations",
|
|
16
|
+
"queries": "./queries",
|
|
17
|
+
"output": {
|
|
18
|
+
"ir": "./syncular.ir.json",
|
|
19
|
+
"module": "./src/syncular.generated.ts",
|
|
20
|
+
"queries": "./src/syncular.queries.ts"
|
|
21
|
+
},
|
|
22
|
+
"schemaVersions": [{ "version": 1, "through": "0001_initial" }],
|
|
23
|
+
"tables": [{ "name": "notes", "scopes": ["list:{list_id}"] }],
|
|
24
|
+
"subscriptions": [
|
|
25
|
+
{
|
|
26
|
+
"name": "notesInList",
|
|
27
|
+
"table": "notes",
|
|
28
|
+
"scopes": { "list_id": ["{listId}"] }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const STARTER_MIGRATION = `-- Your first table. typegen reads this for the schema SHAPE (column types,
|
|
35
|
+
-- primary key); the server manages its own sync_* tables and never runs it.
|
|
36
|
+
CREATE TABLE notes (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
list_id TEXT NOT NULL,
|
|
39
|
+
body TEXT NOT NULL,
|
|
40
|
+
updated_at_ms INTEGER NOT NULL
|
|
41
|
+
);
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
// A starter NAMED query (the typed .sql read tier). typegen type-checks it
|
|
45
|
+
// against the schema via SQLite and emits a typed \`notesInList(client, listId)\`
|
|
46
|
+
// + \`NotesInListRow\` into src/syncular.queries.ts. :listId infers to TEXT
|
|
47
|
+
// (compared against notes.list_id).
|
|
48
|
+
const STARTER_QUERY = `-- Every note in a list, id-ordered. Typed by SQLite against your schema.
|
|
49
|
+
SELECT id, list_id, body, updated_at_ms
|
|
50
|
+
FROM notes
|
|
51
|
+
WHERE list_id = :listId
|
|
52
|
+
ORDER BY id
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export interface InitResult {
|
|
56
|
+
readonly written: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Errors that carry a friendly, actionable message for the CLI. */
|
|
60
|
+
export class InitError extends Error {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'InitError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create the starter files under `dir`. Refuses to overwrite an existing
|
|
69
|
+
* manifest or `migrations/0001_initial/up.sql`.
|
|
70
|
+
*/
|
|
71
|
+
export function initProject(dir: string): InitResult {
|
|
72
|
+
const root = resolve(dir);
|
|
73
|
+
const manifestPath = join(root, MANIFEST_FILENAME);
|
|
74
|
+
const migrationDir = join(root, 'migrations', '0001_initial');
|
|
75
|
+
const migrationPath = join(migrationDir, 'up.sql');
|
|
76
|
+
const queriesDir = join(root, 'queries');
|
|
77
|
+
const queryPath = join(queriesDir, 'notes-in-list.sql');
|
|
78
|
+
|
|
79
|
+
if (existsSync(manifestPath)) {
|
|
80
|
+
throw new InitError(
|
|
81
|
+
`${manifestPath} already exists — refusing to overwrite. ` +
|
|
82
|
+
'Edit it directly, or run `syncular generate`.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (existsSync(migrationPath)) {
|
|
86
|
+
throw new InitError(
|
|
87
|
+
`${migrationPath} already exists — refusing to overwrite.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (existsSync(queryPath)) {
|
|
91
|
+
throw new InitError(`${queryPath} already exists — refusing to overwrite.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mkdirSync(migrationDir, { recursive: true });
|
|
95
|
+
mkdirSync(queriesDir, { recursive: true });
|
|
96
|
+
writeFileSync(manifestPath, STARTER_MANIFEST, 'utf8');
|
|
97
|
+
writeFileSync(migrationPath, STARTER_MIGRATION, 'utf8');
|
|
98
|
+
writeFileSync(queryPath, STARTER_QUERY, 'utf8');
|
|
99
|
+
return { written: [manifestPath, migrationPath, queryPath] };
|
|
100
|
+
}
|
package/src/ir.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral schema IR (REVISE B5).
|
|
3
|
+
*
|
|
4
|
+
* The IR is a versioned, language-neutral JSON document: tables with the
|
|
5
|
+
* SPEC §2.4 column types (+ nullability + primary key), scope patterns
|
|
6
|
+
* (§3.1), the schema-version history (§1.5 gating), subscription templates,
|
|
7
|
+
* and a reserved `extensions` slot (the WP-49 apply/read-model hooks home —
|
|
8
|
+
* empty today, present in the schema so later emitters share one shape).
|
|
9
|
+
* Serialization is deterministic: fixed key order, sorted extension keys,
|
|
10
|
+
* 2-space indent, trailing newline — so the IR file diffs cleanly and the
|
|
11
|
+
* Swift/Kotlin emitters can hash it the same way the TS emitter does.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
export const IR_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/** The §2.4 column types (wire tags 1..8, in this order; tag 7 = blob_ref,
|
|
18
|
+
* §5.9; tag 8 = crdt, §5.10). */
|
|
19
|
+
export type IrColumnType =
|
|
20
|
+
| 'string'
|
|
21
|
+
| 'integer'
|
|
22
|
+
| 'float'
|
|
23
|
+
| 'boolean'
|
|
24
|
+
| 'json'
|
|
25
|
+
| 'bytes'
|
|
26
|
+
| 'blob_ref'
|
|
27
|
+
| 'crdt';
|
|
28
|
+
|
|
29
|
+
export interface IrColumn {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly type: IrColumnType;
|
|
32
|
+
readonly nullable: boolean;
|
|
33
|
+
/** For a `crdt` column (§5.10.1): the merger name (default `yjs-doc`).
|
|
34
|
+
* Present only for `crdt`; serialized after `nullable` when set. */
|
|
35
|
+
readonly crdtType?: string;
|
|
36
|
+
/** §5.11: this column is encrypted end-to-end. When set, `type` is `bytes`
|
|
37
|
+
* (the wire type) and `declaredType` is the pre-flip app type. Both are IR
|
|
38
|
+
* metadata (never on the wire, like `crdtType`). Present only for encrypted
|
|
39
|
+
* columns, so non-encrypted columns stay byte-identical in the IR. */
|
|
40
|
+
readonly encrypted?: boolean;
|
|
41
|
+
/** §5.11: the app-side type of an encrypted column. Present iff `encrypted`. */
|
|
42
|
+
readonly declaredType?: IrColumnType;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One §3.1 scope pattern, fully resolved (no re-parsing downstream). */
|
|
46
|
+
export interface IrScope {
|
|
47
|
+
readonly pattern: string;
|
|
48
|
+
readonly variable: string;
|
|
49
|
+
readonly column: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One local secondary index declared by a `CREATE [UNIQUE] INDEX` migration.
|
|
54
|
+
* Applied on BOTH sides: the client materializes it in its local SQLite, and
|
|
55
|
+
* the server creates it on the relational per-app row table
|
|
56
|
+
* (DESIGN-relational-server-storage.md "user indexes"; the sync read path
|
|
57
|
+
* itself still uses the scope inverted-index). `columns` preserves declared
|
|
58
|
+
* order (a compound index is order-sensitive). Index order within a table is
|
|
59
|
+
* declaration order.
|
|
60
|
+
*/
|
|
61
|
+
export interface IrIndex {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly columns: readonly string[];
|
|
64
|
+
readonly unique: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IrTable {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly primaryKey: string;
|
|
70
|
+
/** Declaration order — this IS the §2.4 row-codec positional order. */
|
|
71
|
+
readonly columns: readonly IrColumn[];
|
|
72
|
+
readonly scopes: readonly IrScope[];
|
|
73
|
+
/** Local secondary indexes (§migration subset), in declaration order.
|
|
74
|
+
* Additive under irVersion 1: absent/empty for tables that declare none. */
|
|
75
|
+
readonly indexes: readonly IrIndex[];
|
|
76
|
+
/** Reserved per-table hook slot (WP-49); empty object for now. */
|
|
77
|
+
readonly extensions: Readonly<Record<string, unknown>>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type IrScopeValue =
|
|
81
|
+
| { readonly kind: 'literal'; readonly value: string }
|
|
82
|
+
| { readonly kind: 'parameter'; readonly name: string };
|
|
83
|
+
|
|
84
|
+
export interface IrSubscriptionScope {
|
|
85
|
+
readonly variable: string;
|
|
86
|
+
readonly values: readonly IrScopeValue[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface IrSubscription {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly table: string;
|
|
92
|
+
/** Sorted by variable for stable diffs. */
|
|
93
|
+
readonly scopes: readonly IrSubscriptionScope[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface IrSchemaVersion {
|
|
97
|
+
readonly version: number;
|
|
98
|
+
/** Migration names this version added, in application order. */
|
|
99
|
+
readonly migrations: readonly string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface IrDocument {
|
|
103
|
+
readonly irVersion: number;
|
|
104
|
+
/** The current (latest) generated schema version (§1.5). */
|
|
105
|
+
readonly schemaVersion: number;
|
|
106
|
+
readonly schemaVersions: readonly IrSchemaVersion[];
|
|
107
|
+
/** Manifest order — the handler-declared bootstrap order (§4.7). */
|
|
108
|
+
readonly tables: readonly IrTable[];
|
|
109
|
+
readonly subscriptions: readonly IrSubscription[];
|
|
110
|
+
/** Reserved document-level hook slot (WP-49); empty object for now. */
|
|
111
|
+
readonly extensions: Readonly<Record<string, unknown>>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Recursively sort object keys so extension payloads serialize stably. */
|
|
115
|
+
export function canonicalizeExtensions(value: unknown): unknown {
|
|
116
|
+
if (Array.isArray(value)) return value.map(canonicalizeExtensions);
|
|
117
|
+
if (value !== null && typeof value === 'object') {
|
|
118
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(
|
|
119
|
+
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
|
|
120
|
+
);
|
|
121
|
+
const out: Record<string, unknown> = {};
|
|
122
|
+
for (const [key, entry] of entries) {
|
|
123
|
+
out[key] = canonicalizeExtensions(entry);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function canonicalScopeValue(value: IrScopeValue): Record<string, unknown> {
|
|
131
|
+
return value.kind === 'literal'
|
|
132
|
+
? { kind: 'literal', value: value.value }
|
|
133
|
+
: { kind: 'parameter', name: value.name };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Serialize with a fixed key order — byte-deterministic for equal IRs. */
|
|
137
|
+
export function serializeIr(ir: IrDocument): string {
|
|
138
|
+
const doc = {
|
|
139
|
+
irVersion: ir.irVersion,
|
|
140
|
+
schemaVersion: ir.schemaVersion,
|
|
141
|
+
schemaVersions: ir.schemaVersions.map((v) => ({
|
|
142
|
+
version: v.version,
|
|
143
|
+
migrations: v.migrations,
|
|
144
|
+
})),
|
|
145
|
+
tables: ir.tables.map((table) => ({
|
|
146
|
+
name: table.name,
|
|
147
|
+
primaryKey: table.primaryKey,
|
|
148
|
+
columns: table.columns.map((column) => ({
|
|
149
|
+
name: column.name,
|
|
150
|
+
type: column.type,
|
|
151
|
+
nullable: column.nullable,
|
|
152
|
+
// §5.10.1: crdtType is IR metadata (never on the wire); emitted only
|
|
153
|
+
// for crdt columns so non-crdt column diffs stay byte-identical.
|
|
154
|
+
...(column.crdtType !== undefined ? { crdtType: column.crdtType } : {}),
|
|
155
|
+
// §5.11: encrypted/declaredType are IR metadata (never on the wire);
|
|
156
|
+
// emitted only for encrypted columns so non-encrypted diffs stay
|
|
157
|
+
// byte-identical.
|
|
158
|
+
...(column.encrypted === true
|
|
159
|
+
? { encrypted: true, declaredType: column.declaredType }
|
|
160
|
+
: {}),
|
|
161
|
+
})),
|
|
162
|
+
scopes: table.scopes.map((scope) => ({
|
|
163
|
+
pattern: scope.pattern,
|
|
164
|
+
variable: scope.variable,
|
|
165
|
+
column: scope.column,
|
|
166
|
+
})),
|
|
167
|
+
// Indexes are additive under irVersion 1: emitted only when the table
|
|
168
|
+
// declares at least one, so index-free tables (every pre-index manifest)
|
|
169
|
+
// stay byte-identical and --check fresh. Declaration order preserved.
|
|
170
|
+
...(table.indexes.length > 0
|
|
171
|
+
? {
|
|
172
|
+
indexes: table.indexes.map((index) => ({
|
|
173
|
+
name: index.name,
|
|
174
|
+
columns: index.columns,
|
|
175
|
+
unique: index.unique,
|
|
176
|
+
})),
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
179
|
+
extensions: canonicalizeExtensions(table.extensions),
|
|
180
|
+
})),
|
|
181
|
+
subscriptions: ir.subscriptions.map((sub) => ({
|
|
182
|
+
name: sub.name,
|
|
183
|
+
table: sub.table,
|
|
184
|
+
scopes: sub.scopes.map((scope) => ({
|
|
185
|
+
variable: scope.variable,
|
|
186
|
+
values: scope.values.map(canonicalScopeValue),
|
|
187
|
+
})),
|
|
188
|
+
})),
|
|
189
|
+
extensions: canonicalizeExtensions(ir.extensions),
|
|
190
|
+
};
|
|
191
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Hash of the exact IR file bytes; stamped into generated modules. */
|
|
195
|
+
export function irHash(irJson: string): string {
|
|
196
|
+
return `sha256:${createHash('sha256').update(irJson, 'utf8').digest('hex')}`;
|
|
197
|
+
}
|