diff --git a/.changeset/beige-poems-share.md b/.changeset/beige-poems-share.md new file mode 100644 index 000000000..18bea24e7 --- /dev/null +++ b/.changeset/beige-poems-share.md @@ -0,0 +1,5 @@ +--- +'@powersync/service-sync-rules': minor +--- + +Added Schema generators for Kotlin, Swift and DotNet diff --git a/packages/sync-rules/src/index.ts b/packages/sync-rules/src/index.ts index 4a80c9dcf..79e3220d9 100644 --- a/packages/sync-rules/src/index.ts +++ b/packages/sync-rules/src/index.ts @@ -1,15 +1,13 @@ export * from './BucketDescription.js'; -export * from './DartSchemaGenerator.js'; +export * from './BucketParameterQuerier.js'; export * from './errors.js'; export * from './events/SqlEventDescriptor.js'; export * from './events/SqlEventSourceQuery.js'; export * from './ExpressionType.js'; -export * from './generators.js'; export * from './IdSequence.js'; -export * from './JsLegacySchemaGenerator.js'; export * from './json_schema.js'; export * from './request_functions.js'; -export * from './SchemaGenerator.js'; +export * from './schema-generators/schema-generators.js'; export * from './SourceTableInterface.js'; export * from './sql_filters.js'; export * from './sql_functions.js'; @@ -18,7 +16,5 @@ export * from './SqlParameterQuery.js'; export * from './SqlSyncRules.js'; export * from './StaticSchema.js'; export * from './TablePattern.js'; -export * from './TsSchemaGenerator.js'; export * from './types.js'; export * from './utils.js'; -export * from './BucketParameterQuerier.js'; diff --git a/packages/sync-rules/src/DartSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/DartSchemaGenerator.ts similarity index 82% rename from packages/sync-rules/src/DartSchemaGenerator.ts rename to packages/sync-rules/src/schema-generators/DartSchemaGenerator.ts index b1ce0b267..3079bfb4c 100644 --- a/packages/sync-rules/src/DartSchemaGenerator.ts +++ b/packages/sync-rules/src/schema-generators/DartSchemaGenerator.ts @@ -1,7 +1,7 @@ -import { ColumnDefinition, ExpressionType, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from './ExpressionType.js'; +import { ColumnDefinition, ExpressionType } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js'; -import { SqlSyncRules } from './SqlSyncRules.js'; -import { SourceSchema } from './types.js'; export class DartSchemaGenerator extends SchemaGenerator { readonly key = 'dart'; @@ -41,7 +41,7 @@ ${generated.join('\n')} } private generateColumn(column: ColumnDefinition) { - return `Column.${dartColumnType(column)}('${column.name}')`; + return `Column.${this.columnType(column)}('${column.name}')`; } } @@ -97,7 +97,7 @@ export class DartFlutterFlowSchemaGenerator extends SchemaGenerator { view_name: null, local_only: localOnly, insert_only: false, - columns: columns.map(this.generateColumn), + columns: columns.map((c) => this.generateColumn(c)), indexes: [] }; } @@ -105,20 +105,7 @@ export class DartFlutterFlowSchemaGenerator extends SchemaGenerator { private generateColumn(definition: ColumnDefinition): object { return { name: definition.name, - type: dartColumnType(definition) + type: this.columnType(definition) }; } } - -const dartColumnType = (def: ColumnDefinition) => { - const t = def.type; - if (t.typeFlags & TYPE_TEXT) { - return 'text'; - } else if (t.typeFlags & TYPE_REAL) { - return 'real'; - } else if (t.typeFlags & TYPE_INTEGER) { - return 'integer'; - } else { - return 'text'; - } -}; diff --git a/packages/sync-rules/src/schema-generators/DotNetSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/DotNetSchemaGenerator.ts new file mode 100644 index 000000000..b18f7844b --- /dev/null +++ b/packages/sync-rules/src/schema-generators/DotNetSchemaGenerator.ts @@ -0,0 +1,71 @@ +import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; +import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js'; + +export class DotNetSchemaGenerator extends SchemaGenerator { + readonly key = 'dotnet'; + readonly label = '.Net'; + readonly mediaType = 'text/x-csharp'; + readonly fileName = 'Schema.cs'; + + generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string { + const tables = super.getAllTables(source, schema); + + return `using PowerSync.Common.DB.Schema; + +class AppSchema +{ + ${tables.map((table) => this.generateTable(table.name, table.columns, options)).join('\n\n ')} + + public static Schema PowerSyncSchema = new Schema(new Dictionary + { + ${tables.map((table) => `{"${table.name}", ${this.toUpperCaseFirstLetter(table.name)}}`).join(',\n ')} + }); +}`; + } + + private toUpperCaseFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + private generateTable(name: string, columns: ColumnDefinition[], options?: GenerateSchemaOptions): string { + const generated = columns.map((c, i) => { + const last = i === columns.length - 1; + const base = this.generateColumn(c); + let withFormatting: string; + if (last) { + withFormatting = ` ${base}`; + } else { + withFormatting = ` ${base},`; + } + + if (options?.includeTypeComments && c.originalType != null) { + return `${withFormatting} // ${c.originalType}`; + } else { + return withFormatting; + } + }); + return `public static Table ${this.toUpperCaseFirstLetter(name)} = new Table(new Dictionary + { + ${generated.join('\n ')} + });`; + } + + private generateColumn(column: ColumnDefinition): string { + return `{ "${column.name}", ${cSharpColumnType(column)} }`; + } +} + +const cSharpColumnType = (def: ColumnDefinition): string => { + const t = def.type; + if (t.typeFlags & TYPE_TEXT) { + return 'ColumnType.TEXT'; + } else if (t.typeFlags & TYPE_REAL) { + return 'ColumnType.REAL'; + } else if (t.typeFlags & TYPE_INTEGER) { + return 'ColumnType.INTEGER'; + } else { + return 'ColumnType.TEXT'; + } +}; diff --git a/packages/sync-rules/src/JsLegacySchemaGenerator.ts b/packages/sync-rules/src/schema-generators/JsLegacySchemaGenerator.ts similarity index 91% rename from packages/sync-rules/src/JsLegacySchemaGenerator.ts rename to packages/sync-rules/src/schema-generators/JsLegacySchemaGenerator.ts index 9315e3bc8..b1b67f16a 100644 --- a/packages/sync-rules/src/JsLegacySchemaGenerator.ts +++ b/packages/sync-rules/src/schema-generators/JsLegacySchemaGenerator.ts @@ -1,7 +1,7 @@ -import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from './ExpressionType.js'; +import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; import { SchemaGenerator } from './SchemaGenerator.js'; -import { SqlSyncRules } from './SqlSyncRules.js'; -import { SourceSchema } from './types.js'; export class JsLegacySchemaGenerator extends SchemaGenerator { readonly key = 'jsLegacy'; diff --git a/packages/sync-rules/src/schema-generators/KotlinSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/KotlinSchemaGenerator.ts new file mode 100644 index 000000000..64082f3b1 --- /dev/null +++ b/packages/sync-rules/src/schema-generators/KotlinSchemaGenerator.ts @@ -0,0 +1,52 @@ +import { ColumnDefinition } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; +import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js'; + +export class KotlinSchemaGenerator extends SchemaGenerator { + readonly key = 'kotlin'; + readonly label = 'Kotlin'; + readonly mediaType = 'text/x-kotlin'; + readonly fileName = 'schema.kt'; + + generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string { + const tables = super.getAllTables(source, schema); + + return `import com.powersync.db.schema.Column +import com.powersync.db.schema.Schema +import com.powersync.db.schema.Table + +val schema = Schema( + ${tables.map((table) => this.generateTable(table.name, table.columns, options)).join(',\n ')} +)`; + } + + private generateTable(name: string, columns: ColumnDefinition[], options?: GenerateSchemaOptions): string { + const generated = columns.map((c, i) => { + const last = i === columns.length - 1; + const base = this.generateColumn(c); + let withFormatting: string; + if (last) { + withFormatting = ` ${base}`; + } else { + withFormatting = ` ${base},`; + } + + if (options?.includeTypeComments && c.originalType != null) { + return `${withFormatting} // ${c.originalType}`; + } else { + return withFormatting; + } + }); + return `Table( + name = "${name}", + columns = listOf( +${generated.join('\n')} + ) + )`; + } + + private generateColumn(column: ColumnDefinition): string { + return `Column.${this.columnType(column)}("${column.name}")`; + } +} diff --git a/packages/sync-rules/src/SchemaGenerator.ts b/packages/sync-rules/src/schema-generators/SchemaGenerator.ts similarity index 63% rename from packages/sync-rules/src/SchemaGenerator.ts rename to packages/sync-rules/src/schema-generators/SchemaGenerator.ts index 18e111b57..dee26bc11 100644 --- a/packages/sync-rules/src/SchemaGenerator.ts +++ b/packages/sync-rules/src/schema-generators/SchemaGenerator.ts @@ -1,6 +1,6 @@ -import { ColumnDefinition } from './ExpressionType.js'; -import { SqlSyncRules } from './SqlSyncRules.js'; -import { SourceSchema } from './types.js'; +import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; export interface GenerateSchemaOptions { includeTypeComments?: boolean; @@ -38,4 +38,21 @@ export abstract class SchemaGenerator { abstract readonly fileName: string; abstract generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string; + + /** + * @param def The column definition to generate the type for. + * @returns The SDK column type for the given column definition. + */ + columnType(def: ColumnDefinition): string { + const { type } = def; + if (type.typeFlags & TYPE_TEXT) { + return 'text'; + } else if (type.typeFlags & TYPE_REAL) { + return 'real'; + } else if (type.typeFlags & TYPE_INTEGER) { + return 'integer'; + } else { + return 'text'; + } + } } diff --git a/packages/sync-rules/src/schema-generators/SwiftSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/SwiftSchemaGenerator.ts new file mode 100644 index 000000000..8e1b7acae --- /dev/null +++ b/packages/sync-rules/src/schema-generators/SwiftSchemaGenerator.ts @@ -0,0 +1,50 @@ +import { ColumnDefinition } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; +import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js'; + +export class SwiftSchemaGenerator extends SchemaGenerator { + readonly key = 'swift'; + readonly label = 'Swift'; + readonly mediaType = 'text/x-swift'; + readonly fileName = 'schema.swift'; + + generate(source: SqlSyncRules, schema: SourceSchema, options?: GenerateSchemaOptions): string { + const tables = super.getAllTables(source, schema); + + return `import PowerSync + +let schema = Schema( + ${tables.map((table) => this.generateTable(table.name, table.columns, options)).join(',\n ')} +)`; + } + + private generateTable(name: string, columns: ColumnDefinition[], options?: GenerateSchemaOptions): string { + const generated = columns.map((c, i) => { + const last = i === columns.length - 1; + const base = this.generateColumn(c); + let withFormatting: string; + if (last) { + withFormatting = ` ${base}`; + } else { + withFormatting = ` ${base},`; + } + + if (options?.includeTypeComments && c.originalType != null) { + return `${withFormatting} // ${c.originalType}`; + } else { + return withFormatting; + } + }); + return `Table( + name: "${name}", + columns: [ +${generated.join('\n')} + ] + )`; + } + + private generateColumn(column: ColumnDefinition): string { + return `.${this.columnType(column)}("${column.name}")`; + } +} diff --git a/packages/sync-rules/src/TsSchemaGenerator.ts b/packages/sync-rules/src/schema-generators/TsSchemaGenerator.ts similarity index 96% rename from packages/sync-rules/src/TsSchemaGenerator.ts rename to packages/sync-rules/src/schema-generators/TsSchemaGenerator.ts index 1a0dba58c..28a5a93cb 100644 --- a/packages/sync-rules/src/TsSchemaGenerator.ts +++ b/packages/sync-rules/src/schema-generators/TsSchemaGenerator.ts @@ -1,7 +1,7 @@ -import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from './ExpressionType.js'; +import { ColumnDefinition, TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from '../ExpressionType.js'; +import { SqlSyncRules } from '../SqlSyncRules.js'; +import { SourceSchema } from '../types.js'; import { GenerateSchemaOptions, SchemaGenerator } from './SchemaGenerator.js'; -import { SqlSyncRules } from './SqlSyncRules.js'; -import { SourceSchema } from './types.js'; export interface TsSchemaGeneratorOptions { language?: TsSchemaLanguage; diff --git a/packages/sync-rules/src/generators.ts b/packages/sync-rules/src/schema-generators/generators.ts similarity index 51% rename from packages/sync-rules/src/generators.ts rename to packages/sync-rules/src/schema-generators/generators.ts index db4e6c69d..c86a47543 100644 --- a/packages/sync-rules/src/generators.ts +++ b/packages/sync-rules/src/schema-generators/generators.ts @@ -1,11 +1,17 @@ import { DartFlutterFlowSchemaGenerator, DartSchemaGenerator } from './DartSchemaGenerator.js'; +import { DotNetSchemaGenerator } from './DotNetSchemaGenerator.js'; import { JsLegacySchemaGenerator } from './JsLegacySchemaGenerator.js'; +import { KotlinSchemaGenerator } from './KotlinSchemaGenerator.js'; +import { SwiftSchemaGenerator } from './SwiftSchemaGenerator.js'; import { TsSchemaGenerator, TsSchemaLanguage } from './TsSchemaGenerator.js'; export const schemaGenerators = { - ts: new TsSchemaGenerator(), + dart: new DartSchemaGenerator(), + dotNet: new DotNetSchemaGenerator(), + flutterFlow: new DartFlutterFlowSchemaGenerator(), js: new TsSchemaGenerator({ language: TsSchemaLanguage.js }), jsLegacy: new JsLegacySchemaGenerator(), - dart: new DartSchemaGenerator(), - flutterFlow: new DartFlutterFlowSchemaGenerator() + kotlin: new KotlinSchemaGenerator(), + swift: new SwiftSchemaGenerator(), + ts: new TsSchemaGenerator() }; diff --git a/packages/sync-rules/src/schema-generators/schema-generators.ts b/packages/sync-rules/src/schema-generators/schema-generators.ts new file mode 100644 index 000000000..29a89bfa2 --- /dev/null +++ b/packages/sync-rules/src/schema-generators/schema-generators.ts @@ -0,0 +1,8 @@ +export * from './DartSchemaGenerator.js'; +export * from './DotNetSchemaGenerator.js'; +export * from './generators.js'; +export * from './JsLegacySchemaGenerator.js'; +export * from './KotlinSchemaGenerator.js'; +export * from './SchemaGenerator.js'; +export * from './SwiftSchemaGenerator.js'; +export * from './TsSchemaGenerator.js'; diff --git a/packages/sync-rules/test/src/generate_schema.test.ts b/packages/sync-rules/test/src/generate_schema.test.ts index 88998a52b..15c419c75 100644 --- a/packages/sync-rules/test/src/generate_schema.test.ts +++ b/packages/sync-rules/test/src/generate_schema.test.ts @@ -3,9 +3,12 @@ import { DEFAULT_TAG, DartFlutterFlowSchemaGenerator, DartSchemaGenerator, + DotNetSchemaGenerator, JsLegacySchemaGenerator, + KotlinSchemaGenerator, SqlSyncRules, StaticSchema, + SwiftSchemaGenerator, TsSchemaGenerator } from '../../src/index.js'; @@ -176,4 +179,156 @@ export type Database = (typeof AppSchema)['types']; ` ); }); + + test('kotlin', () => { + expect(new KotlinSchemaGenerator().generate(rules, schema)).toEqual(`import com.powersync.db.schema.Column +import com.powersync.db.schema.Schema +import com.powersync.db.schema.Table + +val schema = Schema( + Table( + name = "assets1", + columns = listOf( + Column.text("name"), + Column.integer("count"), + Column.text("owner_id") + ) + ), + Table( + name = "assets2", + columns = listOf( + Column.text("name"), + Column.integer("count"), + Column.text("other_id"), + Column.text("foo") + ) + ) +)`); + + expect(new KotlinSchemaGenerator().generate(rules, schema, { includeTypeComments: true })) + .toEqual(`import com.powersync.db.schema.Column +import com.powersync.db.schema.Schema +import com.powersync.db.schema.Table + +val schema = Schema( + Table( + name = "assets1", + columns = listOf( + Column.text("name"), // text + Column.integer("count"), // int4 + Column.text("owner_id") // uuid + ) + ), + Table( + name = "assets2", + columns = listOf( + Column.text("name"), // text + Column.integer("count"), // int4 + Column.text("other_id"), // uuid + Column.text("foo") + ) + ) +)`); + }); + + test('swift', () => { + expect(new SwiftSchemaGenerator().generate(rules, schema)).toEqual(`import PowerSync + +let schema = Schema( + Table( + name: "assets1", + columns: [ + .text("name"), + .integer("count"), + .text("owner_id") + ] + ), + Table( + name: "assets2", + columns: [ + .text("name"), + .integer("count"), + .text("other_id"), + .text("foo") + ] + ) +)`); + + expect(new SwiftSchemaGenerator().generate(rules, schema, { includeTypeComments: true })).toEqual(`import PowerSync + +let schema = Schema( + Table( + name: "assets1", + columns: [ + .text("name"), // text + .integer("count"), // int4 + .text("owner_id") // uuid + ] + ), + Table( + name: "assets2", + columns: [ + .text("name"), // text + .integer("count"), // int4 + .text("other_id"), // uuid + .text("foo") + ] + ) +)`); + }); + + test('dotnet', () => { + expect(new DotNetSchemaGenerator().generate(rules, schema)).toEqual(`using PowerSync.Common.DB.Schema; + +class AppSchema +{ + public static Table Assets1 = new Table(new Dictionary + { + { "name", ColumnType.TEXT }, + { "count", ColumnType.INTEGER }, + { "owner_id", ColumnType.TEXT } + }); + + public static Table Assets2 = new Table(new Dictionary + { + { "name", ColumnType.TEXT }, + { "count", ColumnType.INTEGER }, + { "other_id", ColumnType.TEXT }, + { "foo", ColumnType.TEXT } + }); + + public static Schema PowerSyncSchema = new Schema(new Dictionary + { + {"assets1", Assets1}, + {"assets2", Assets2} + }); +}`); + + expect(new DotNetSchemaGenerator().generate(rules, schema, { includeTypeComments: true })) + .toEqual(`using PowerSync.Common.DB.Schema; + +class AppSchema +{ + public static Table Assets1 = new Table(new Dictionary + { + { "name", ColumnType.TEXT }, // text + { "count", ColumnType.INTEGER }, // int4 + { "owner_id", ColumnType.TEXT } // uuid + }); + + public static Table Assets2 = new Table(new Dictionary + { + { "name", ColumnType.TEXT }, // text + { "count", ColumnType.INTEGER }, // int4 + { "other_id", ColumnType.TEXT }, // uuid + { "foo", ColumnType.TEXT } + }); + + public static Schema PowerSyncSchema = new Schema(new Dictionary + { + {"assets1", Assets1}, + {"assets2", Assets2} + }); +}`); + }); });