From ddb342b823e77593ea68a37bfa84711a2a656015 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Wed, 14 Aug 2024 16:58:04 +0200 Subject: [PATCH 1/6] Add TableV2 / TypeScript schema generator. --- packages/sync-rules/src/TsSchemaGenerator.ts | 50 +++++++++++++++++++ packages/sync-rules/src/index.ts | 1 + .../sync-rules/test/src/sync_rules.test.ts | 38 +++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/sync-rules/src/TsSchemaGenerator.ts diff --git a/packages/sync-rules/src/TsSchemaGenerator.ts b/packages/sync-rules/src/TsSchemaGenerator.ts new file mode 100644 index 000000000..5531badb7 --- /dev/null +++ b/packages/sync-rules/src/TsSchemaGenerator.ts @@ -0,0 +1,50 @@ +import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from './ExpressionType.js'; +import { SchemaGenerator } from './SchemaGenerator.js'; +import { SqlSyncRules } from './SqlSyncRules.js'; +import { SourceSchema } from './types.js'; + +export class TsSchemaGenerator extends SchemaGenerator { + readonly key = 'ts'; + readonly label = 'TypeScript'; + readonly mediaType = 'application/typescript'; + readonly fileName = 'schema.ts'; + + generate(source: SqlSyncRules, schema: SourceSchema): string { + const tables = super.getAllTables(source, schema); + + return `import { column, Schema, TableV2 } from '@powersync/web'; +// OR: import { column, Schema, TableV2 } from '@powersync/react-native'; + +${tables.map((table) => this.generateTable(table.name, table.columns)).join('\n\n')} + +export const AppSchema = new Schema({ + ${tables.map((table) => table.name).join(',\n ')} +}); + +export type Database = (typeof AppSchema)['types']; +`; + } + + private generateTable(name: string, columns: ColumnDefinition[]): string { + return `const ${name} = new TableV2( + { + // id column (text) is automatically included + ${columns.map((c) => this.generateColumn(c)).join(',\n ')} + }, + { indexes: {} } +);`; + } + + private generateColumn(column: ColumnDefinition) { + const t = column.type; + if (t.typeFlags & TYPE_TEXT) { + return `${column.name}: column.text`; + } else if (t.typeFlags & TYPE_REAL) { + return `${column.name}: column.real`; + } else if (t.typeFlags & TYPE_INTEGER) { + return `${column.name}: column.integer`; + } else { + return `${column.name}: column.text`; + } + } +} diff --git a/packages/sync-rules/src/index.ts b/packages/sync-rules/src/index.ts index 78ba6b593..23425667c 100644 --- a/packages/sync-rules/src/index.ts +++ b/packages/sync-rules/src/index.ts @@ -14,6 +14,7 @@ export * from './ExpressionType.js'; export * from './SchemaGenerator.js'; export * from './DartSchemaGenerator.js'; export * from './JsSchemaGenerator.js'; +export * from './TsSchemaGenerator.js'; export * from './generators.js'; export * from './SqlDataQuery.js'; export * from './request_functions.js'; diff --git a/packages/sync-rules/test/src/sync_rules.test.ts b/packages/sync-rules/test/src/sync_rules.test.ts index 3c4fc5f12..bfe2713cb 100644 --- a/packages/sync-rules/test/src/sync_rules.test.ts +++ b/packages/sync-rules/test/src/sync_rules.test.ts @@ -5,8 +5,10 @@ import { DartSchemaGenerator, JsSchemaGenerator, SqlSyncRules, - StaticSchema + StaticSchema, + TsSchemaGenerator } from '../../src/index.js'; + import { ASSETS, BASIC_SCHEMA, TestSourceTable, USERS, normalizeTokenParameters } from './util.js'; describe('sync rules', () => { @@ -781,5 +783,39 @@ bucket_definitions: }) ]) `); + + expect(new TsSchemaGenerator().generate(rules, schema)).toEqual( + `import { column, Schema, TableV2 } from '@powersync/web'; +// OR: import { column, Schema, TableV2 } from '@powersync/react-native'; + +const assets1 = new TableV2( + { + // id column (text) is automatically included + name: column.text, + count: column.integer, + owner_id: column.text + }, + { indexes: {} } +); + +const assets2 = new TableV2( + { + // id column (text) is automatically included + name: column.text, + count: column.integer, + other_id: column.text, + foo: column.text + }, + { indexes: {} } +); + +export const AppSchema = new Schema({ + assets1, + assets2 +}); + +export type Database = (typeof AppSchema)['types']; +` + ); }); }); From 2ae87113afa255937c6f2f821b2c7cdd7d1a5cd6 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Wed, 14 Aug 2024 17:00:19 +0200 Subject: [PATCH 2/6] Add changeset. --- .changeset/thirty-bikes-move.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thirty-bikes-move.md diff --git a/.changeset/thirty-bikes-move.md b/.changeset/thirty-bikes-move.md new file mode 100644 index 000000000..4451d3391 --- /dev/null +++ b/.changeset/thirty-bikes-move.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-sync-rules': patch +--- + +Add TypeScript/TableV2 schema generator From 683e1d0e50021edf678573c710505f9707546899 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 15 Aug 2024 10:07:56 +0200 Subject: [PATCH 3/6] Make some export options configurable. --- packages/sync-rules/src/TsSchemaGenerator.ts | 35 +++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/sync-rules/src/TsSchemaGenerator.ts b/packages/sync-rules/src/TsSchemaGenerator.ts index 5531badb7..7bbc4621a 100644 --- a/packages/sync-rules/src/TsSchemaGenerator.ts +++ b/packages/sync-rules/src/TsSchemaGenerator.ts @@ -3,17 +3,24 @@ import { SchemaGenerator } from './SchemaGenerator.js'; import { SqlSyncRules } from './SqlSyncRules.js'; import { SourceSchema } from './types.js'; +export interface TsSchemaGeneratorOptions { + language?: 'ts' | 'js'; + imports?: 'web' | 'react-native' | 'auto'; +} export class TsSchemaGenerator extends SchemaGenerator { readonly key = 'ts'; readonly label = 'TypeScript'; readonly mediaType = 'application/typescript'; readonly fileName = 'schema.ts'; + constructor(public readonly options: TsSchemaGeneratorOptions = {}) { + super(); + } + generate(source: SqlSyncRules, schema: SourceSchema): string { const tables = super.getAllTables(source, schema); - return `import { column, Schema, TableV2 } from '@powersync/web'; -// OR: import { column, Schema, TableV2 } from '@powersync/react-native'; + return `${this.generateImports()} ${tables.map((table) => this.generateTable(table.name, table.columns)).join('\n\n')} @@ -21,8 +28,28 @@ export const AppSchema = new Schema({ ${tables.map((table) => table.name).join(',\n ')} }); -export type Database = (typeof AppSchema)['types']; -`; +${this.generateTypeExports()}`; + } + + private generateTypeExports() { + const lang = this.options.language ?? 'ts'; + if (lang == 'ts') { + return `export type Database = (typeof AppSchema)['types'];\n`; + } else { + return ``; + } + } + + private generateImports() { + const importStyle = this.options.imports ?? 'auto'; + if (importStyle == 'web') { + return `import { column, Schema, TableV2 } from '@powersync/web';`; + } else if (importStyle == 'react-native') { + return `import { column, Schema, TableV2 } from '@powersync/react-native';`; + } else { + return `import { column, Schema, TableV2 } from '@powersync/web'; +// OR: import { column, Schema, TableV2 } from '@powersync/react-native';`; + } } private generateTable(name: string, columns: ColumnDefinition[]): string { From 611ec0196c0ee1089c2b09725d416d9275e29ff9 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 15 Aug 2024 12:02:53 +0200 Subject: [PATCH 4/6] Use enums. --- packages/sync-rules/src/TsSchemaGenerator.ts | 28 +++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/sync-rules/src/TsSchemaGenerator.ts b/packages/sync-rules/src/TsSchemaGenerator.ts index 7bbc4621a..8e62932af 100644 --- a/packages/sync-rules/src/TsSchemaGenerator.ts +++ b/packages/sync-rules/src/TsSchemaGenerator.ts @@ -4,9 +4,25 @@ import { SqlSyncRules } from './SqlSyncRules.js'; import { SourceSchema } from './types.js'; export interface TsSchemaGeneratorOptions { - language?: 'ts' | 'js'; - imports?: 'web' | 'react-native' | 'auto'; + language?: TsSchemaLanguage; + imports?: TsSchemaImports; } + +export enum TsSchemaLanguage { + ts = 'ts', + /** Excludes types from the generated schema. */ + js = 'js' +} + +export enum TsSchemaImports { + web = 'web', + reactNative = 'reactNative', + /** + * Emits imports for `@powersync/web`, with comments for `@powersync/react-native`. + */ + auto = 'auto' +} + export class TsSchemaGenerator extends SchemaGenerator { readonly key = 'ts'; readonly label = 'TypeScript'; @@ -32,8 +48,8 @@ ${this.generateTypeExports()}`; } private generateTypeExports() { - const lang = this.options.language ?? 'ts'; - if (lang == 'ts') { + const lang = this.options.language ?? TsSchemaLanguage.ts; + if (lang == TsSchemaLanguage.ts) { return `export type Database = (typeof AppSchema)['types'];\n`; } else { return ``; @@ -42,9 +58,9 @@ ${this.generateTypeExports()}`; private generateImports() { const importStyle = this.options.imports ?? 'auto'; - if (importStyle == 'web') { + if (importStyle == TsSchemaImports.web) { return `import { column, Schema, TableV2 } from '@powersync/web';`; - } else if (importStyle == 'react-native') { + } else if (importStyle == TsSchemaImports.reactNative) { return `import { column, Schema, TableV2 } from '@powersync/react-native';`; } else { return `import { column, Schema, TableV2 } from '@powersync/web'; From b1748240ea18056559a4eb7a80592b1206e918eb Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 15 Aug 2024 12:19:21 +0200 Subject: [PATCH 5/6] Proper metadata for TypeScript schema generator. --- ...enerator.ts => JsLegacySchemaGenerator.ts} | 6 ++--- packages/sync-rules/src/TsSchemaGenerator.ts | 25 ++++++++++++++----- packages/sync-rules/src/generators.ts | 7 ++++-- packages/sync-rules/src/index.ts | 2 +- .../sync-rules/test/src/sync_rules.test.ts | 4 +-- 5 files changed, 30 insertions(+), 14 deletions(-) rename packages/sync-rules/src/{JsSchemaGenerator.ts => JsLegacySchemaGenerator.ts} (89%) diff --git a/packages/sync-rules/src/JsSchemaGenerator.ts b/packages/sync-rules/src/JsLegacySchemaGenerator.ts similarity index 89% rename from packages/sync-rules/src/JsSchemaGenerator.ts rename to packages/sync-rules/src/JsLegacySchemaGenerator.ts index 2e22876d2..8e6fa3985 100644 --- a/packages/sync-rules/src/JsSchemaGenerator.ts +++ b/packages/sync-rules/src/JsLegacySchemaGenerator.ts @@ -3,10 +3,10 @@ import { SchemaGenerator } from './SchemaGenerator.js'; import { SqlSyncRules } from './SqlSyncRules.js'; import { SourceSchema } from './types.js'; -export class JsSchemaGenerator extends SchemaGenerator { +export class JsLegacySchemaGenerator extends SchemaGenerator { readonly key = 'js'; - readonly label = 'JavaScript'; - readonly mediaType = 'application/javascript'; + readonly label = 'JavaScript (legacy syntax)'; + readonly mediaType = 'text/javascript'; readonly fileName = 'schema.js'; generate(source: SqlSyncRules, schema: SourceSchema): string { diff --git a/packages/sync-rules/src/TsSchemaGenerator.ts b/packages/sync-rules/src/TsSchemaGenerator.ts index 8e62932af..10cf12209 100644 --- a/packages/sync-rules/src/TsSchemaGenerator.ts +++ b/packages/sync-rules/src/TsSchemaGenerator.ts @@ -24,13 +24,27 @@ export enum TsSchemaImports { } export class TsSchemaGenerator extends SchemaGenerator { - readonly key = 'ts'; - readonly label = 'TypeScript'; - readonly mediaType = 'application/typescript'; - readonly fileName = 'schema.ts'; + readonly key: string; + readonly fileName: string; + readonly mediaType: string; + readonly label: string; + + readonly language: TsSchemaLanguage; constructor(public readonly options: TsSchemaGeneratorOptions = {}) { super(); + + this.language = options.language ?? TsSchemaLanguage.ts; + this.key = this.language; + if (this.language == TsSchemaLanguage.ts) { + this.fileName = 'schema.ts'; + this.mediaType = 'text/typescript'; + this.label = 'TypeScript'; + } else { + this.fileName = 'schema.js'; + this.mediaType = 'text/javascript'; + this.label = 'JavaScript'; + } } generate(source: SqlSyncRules, schema: SourceSchema): string { @@ -48,8 +62,7 @@ ${this.generateTypeExports()}`; } private generateTypeExports() { - const lang = this.options.language ?? TsSchemaLanguage.ts; - if (lang == TsSchemaLanguage.ts) { + if (this.language == TsSchemaLanguage.ts) { return `export type Database = (typeof AppSchema)['types'];\n`; } else { return ``; diff --git a/packages/sync-rules/src/generators.ts b/packages/sync-rules/src/generators.ts index da3e06845..254f9f78c 100644 --- a/packages/sync-rules/src/generators.ts +++ b/packages/sync-rules/src/generators.ts @@ -1,7 +1,10 @@ import { DartSchemaGenerator } from './DartSchemaGenerator.js'; -import { JsSchemaGenerator } from './JsSchemaGenerator.js'; +import { JsLegacySchemaGenerator } from './JsLegacySchemaGenerator.js'; +import { TsSchemaGenerator, TsSchemaLanguage } from './TsSchemaGenerator.js'; export const schemaGenerators = { - js: new JsSchemaGenerator(), + ts: new TsSchemaGenerator(), + js: new TsSchemaGenerator({ language: TsSchemaLanguage.js }), + jsLegacy: new JsLegacySchemaGenerator(), dart: new DartSchemaGenerator() }; diff --git a/packages/sync-rules/src/index.ts b/packages/sync-rules/src/index.ts index 23425667c..d72a1e58c 100644 --- a/packages/sync-rules/src/index.ts +++ b/packages/sync-rules/src/index.ts @@ -13,7 +13,7 @@ export * from './StaticSchema.js'; export * from './ExpressionType.js'; export * from './SchemaGenerator.js'; export * from './DartSchemaGenerator.js'; -export * from './JsSchemaGenerator.js'; +export * from './JsLegacySchemaGenerator.js'; export * from './TsSchemaGenerator.js'; export * from './generators.js'; export * from './SqlDataQuery.js'; diff --git a/packages/sync-rules/test/src/sync_rules.test.ts b/packages/sync-rules/test/src/sync_rules.test.ts index bfe2713cb..13abed5bb 100644 --- a/packages/sync-rules/test/src/sync_rules.test.ts +++ b/packages/sync-rules/test/src/sync_rules.test.ts @@ -3,7 +3,7 @@ import { DEFAULT_SCHEMA, DEFAULT_TAG, DartSchemaGenerator, - JsSchemaGenerator, + JsLegacySchemaGenerator, SqlSyncRules, StaticSchema, TsSchemaGenerator @@ -763,7 +763,7 @@ bucket_definitions: ]); `); - expect(new JsSchemaGenerator().generate(rules, schema)).toEqual(`new Schema([ + expect(new JsLegacySchemaGenerator().generate(rules, schema)).toEqual(`new Schema([ new Table({ name: 'assets1', columns: [ From 756ad8c8ac6a717c4d9c1327ed2b7e1eaad3330d Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 15 Aug 2024 13:01:30 +0200 Subject: [PATCH 6/6] Rename key. --- packages/sync-rules/src/JsLegacySchemaGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sync-rules/src/JsLegacySchemaGenerator.ts b/packages/sync-rules/src/JsLegacySchemaGenerator.ts index 8e6fa3985..9315e3bc8 100644 --- a/packages/sync-rules/src/JsLegacySchemaGenerator.ts +++ b/packages/sync-rules/src/JsLegacySchemaGenerator.ts @@ -4,7 +4,7 @@ import { SqlSyncRules } from './SqlSyncRules.js'; import { SourceSchema } from './types.js'; export class JsLegacySchemaGenerator extends SchemaGenerator { - readonly key = 'js'; + readonly key = 'jsLegacy'; readonly label = 'JavaScript (legacy syntax)'; readonly mediaType = 'text/javascript'; readonly fileName = 'schema.js';