@syncular/typegen 0.1.3 → 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
package/src/index.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /**
2
- * @syncular/typegen - Generate TypeScript types from migrations
3
- *
4
- * Creates type definitions by:
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 './app-contract';
11
- export * from './checksums';
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 './introspect';
14
- export * from './map-types';
15
- export * from './render';
16
- export * from './types';
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
+ }