@harperfast/schema-codegen 1.0.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 ADDED
@@ -0,0 +1,107 @@
1
+ # @HarperFast/Schema-Codegen
2
+
3
+ Schema Codegen will generate TypeScript types for your GraphQL schemas, making it easier to work with your data in TypeScript and JavaScript applications.
4
+
5
+ ## Installation
6
+
7
+ Install this with your favorite package manager!
8
+
9
+ **Warning**: I haven't actually published this yet. :)
10
+
11
+ ```bash
12
+ npm install --save @harperfast/schema-codegen
13
+ ```
14
+
15
+ Drop this in your Harper application's config.yaml:
16
+
17
+ ```yaml
18
+ '@harperfast/schema-codegen':
19
+ package: '@harperfast/schema-codegen'
20
+ globalTypes: 'schemas/globalTypes.d.ts'
21
+ schemaTypes: 'schemas/types.ts'
22
+ ```
23
+
24
+ Alternatively, if you are using pure JavaScript, you can generate JSDoc instead:
25
+
26
+ ```yaml
27
+ '@harperfast/schema-codegen':
28
+ package: '@harperfast/schema-codegen'
29
+ jsdoc: 'schemas/jsdocTypes.js'
30
+ ```
31
+
32
+ When you `harper dev`, it will generate types based on the schema that's actually in your Harper database. If you change the schema, we will automatically regenerate the types for you.
33
+
34
+ ## Example
35
+
36
+ For example, here's a tracks.graphql schema:
37
+
38
+ ```graphql
39
+ type Tracks @table @sealed {
40
+ id: ID @primaryKey
41
+ name: String! @indexed
42
+ mp3: Blob
43
+ }
44
+ ```
45
+
46
+ Next to it, a schemas/types.ts file will get generated with this:
47
+
48
+ ```typescript
49
+ /**
50
+ Generated from HarperDB schema
51
+ Manual changes will be lost!
52
+ > harper dev .
53
+ */
54
+ export interface Track {
55
+ id: string;
56
+ name: string;
57
+ mp3?: any;
58
+ }
59
+
60
+ export type NewTrack = Omit<Track, 'id'>;
61
+ export type Tracks = Track[];
62
+ export type { Track as TrackRecord };
63
+ export type TrackRecords = Track[];
64
+ export type NewTrackRecord = Omit<Track, 'id'>;
65
+ ```
66
+
67
+ An ambient declaration will also be generated in globalTypes.d.ts to enhance the global `tables` and `databases` from Harper:
68
+
69
+ ```typescript
70
+ /**
71
+ Generated from your schema files
72
+ Manual changes will be lost!
73
+ > harper dev .
74
+ */
75
+ import type { Table } from 'harperdb';
76
+ import type { Track } from './types.ts';
77
+
78
+ declare module 'harperdb' {
79
+ export const tables: {
80
+ Tracks: { new(...args: any[]): Table<Track> };
81
+ };
82
+ export const databases: {
83
+ data: {
84
+ Tracks: { new(...args: any[]): Table<Track> };
85
+ };
86
+ };
87
+ }
88
+ ```
89
+
90
+ ## Development
91
+
92
+ This code uses type stripping, so as long as you use a compatible version of Node, nothing needs to be compiled.
93
+
94
+ To use this in an application, first link it:
95
+
96
+ ```bash
97
+ git clone git@github.com:HarperFast/schema-codegen.git
98
+ cd schema-codegen
99
+ npm link
100
+ ```
101
+
102
+ Then cd to your awesome application you want to test this with:
103
+
104
+ ```bash
105
+ cd ~/my-awesome-app
106
+ npm link @harperfast/schema-codegen
107
+ ```
package/config.yaml ADDED
@@ -0,0 +1 @@
1
+ extensionModule: ./extensionModule.ts
@@ -0,0 +1,58 @@
1
+ import type { Scope } from 'harperdb';
2
+ import { setLogger } from './utils/logger.ts';
3
+ import { regenerateAll } from './utils/regenerateAll.ts';
4
+ import { sleep } from './utils/sleep.ts';
5
+
6
+ export const suppressHandleApplicationWarning = true;
7
+
8
+ export async function handleApplication(scope: Scope) {
9
+ setLogger(scope.logger);
10
+
11
+ if (!process.env.DEV_MODE) {
12
+ scope.logger.trace?.('@harperfast/schema-codegen skipping execution outside of dev mode');
13
+ return;
14
+ }
15
+
16
+ const watchConfig = scope.options.get(['watch']);
17
+ const shouldWatch = watchConfig === true || watchConfig === undefined;
18
+ const globalTypes = (scope.options.get(['globalTypes']) as string) || './schema.globalTypes.d.ts';
19
+ const schemaTypes = (scope.options.get(['schemaTypes']) as string) || './schema.types.ts';
20
+ const jsdoc = scope.options.get(['jsdoc']) as string | undefined;
21
+
22
+ if (shouldWatch) {
23
+ scope.on('close', scopeClosed);
24
+ }
25
+
26
+ // Do not await this.
27
+ sleep(500)
28
+ .then(() => {
29
+ // Initial generation
30
+ regenerateAll(globalTypes, schemaTypes, jsdoc);
31
+
32
+ if (shouldWatch) {
33
+ // Watch for schema/database changes via events
34
+ scope.databaseEvents.on('updateTable', updateTable);
35
+ scope.databaseEvents.on('dropTable', dropTable);
36
+ scope.databaseEvents.on('dropDatabase', dropDatabase);
37
+ }
38
+ });
39
+
40
+ function updateTable() {
41
+ regenerateAll(globalTypes, schemaTypes, jsdoc);
42
+ }
43
+
44
+ function dropTable() {
45
+ regenerateAll(globalTypes, schemaTypes, jsdoc);
46
+ }
47
+
48
+ function dropDatabase() {
49
+ regenerateAll(globalTypes, schemaTypes, jsdoc);
50
+ }
51
+
52
+ function scopeClosed() {
53
+ scope.databaseEvents.off('updateTable', updateTable);
54
+ scope.databaseEvents.off('dropTable', dropTable);
55
+ scope.databaseEvents.off('dropDatabase', dropDatabase);
56
+ scope.off('close', scopeClosed);
57
+ }
58
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@harperfast/schema-codegen",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "commitlint": "commitlint --edit",
7
+ "format:check": "dprint check",
8
+ "format:fix": "dprint fmt",
9
+ "format:staged": "dprint check --staged --allow-no-files",
10
+ "lint:check": "oxlint .",
11
+ "lint:fix": "oxlint . --fix",
12
+ "test": "vitest run",
13
+ "test:coverage": "vitest run --coverage",
14
+ "test:watch": "vitest"
15
+ },
16
+ "files": [
17
+ "extensionModule.ts",
18
+ "config.yaml",
19
+ "utils/",
20
+ "!utils/**/*.test.ts"
21
+ ],
22
+ "devDependencies": {
23
+ "@commitlint/cli": "^20.0.0",
24
+ "@commitlint/config-conventional": "^20.0.0",
25
+ "@semantic-release/commit-analyzer": "^13.0.1",
26
+ "@semantic-release/git": "^10.0.1",
27
+ "@semantic-release/github": "^12.0.2",
28
+ "@semantic-release/npm": "^13.1.1",
29
+ "@semantic-release/release-notes-generator": "^14.1.0",
30
+ "@types/node": "^24.10.11",
31
+ "@types/react": "^19.2.10",
32
+ "@vitest/coverage-v8": "^4.0.18",
33
+ "conventional-changelog-conventionalcommits": "^9.1.0",
34
+ "dprint": "^0.52.0",
35
+ "harperdb": "^4.7.20",
36
+ "oxlint": "^1.51.0",
37
+ "semantic-release": "^25.0.2",
38
+ "vitest": "^4.0.18"
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ import type { Table } from 'harperdb';
2
+ import { databases as hdbDatabases } from 'harperdb';
3
+
4
+ export function collectTables() {
5
+ const tablesList: (Table & { databaseName: string })[] = [];
6
+ for (const dbName of Object.keys(hdbDatabases || {})) {
7
+ const tables = (hdbDatabases as any)[dbName];
8
+ for (const tableName of Object.keys(tables || {})) {
9
+ const TableClass = tables[tableName];
10
+ if (!TableClass?.attributes) { continue; }
11
+ (TableClass as any).databaseName = dbName;
12
+ tablesList.push(TableClass as Table & { databaseName: string });
13
+ }
14
+ }
15
+ return tablesList;
16
+ }
@@ -0,0 +1,48 @@
1
+ import type { Table } from 'harperdb';
2
+ import { isNullable } from './isNullable.ts';
3
+ import { mapType } from './mapType.ts';
4
+ import { singularize } from './singularize.ts';
5
+
6
+ export function generateInterface(table: Table & { databaseName?: string }) {
7
+ const pluralRaw = table.tableName;
8
+ const singularRaw = singularize(pluralRaw);
9
+ const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : '';
10
+ const plural = `${dbPrefix}${pluralRaw}`;
11
+ const singular = `${dbPrefix}${singularRaw}`;
12
+ const isDifferent = plural !== singular;
13
+
14
+ let code = `\nexport interface ${singular} {\n`;
15
+ const primaryKeys: string[] = [];
16
+ for (const attribute of table.attributes || []) {
17
+ const type = mapType(attribute);
18
+ const primaryKey = !!attribute.isPrimaryKey;
19
+ const nullable = !primaryKey && isNullable(attribute);
20
+ code += `\t${attribute.name}${nullable ? '?' : ''}: ${type};\n`;
21
+ if (primaryKey) {
22
+ primaryKeys.push(attribute.name!);
23
+ }
24
+ }
25
+ code += `}\n\n`;
26
+
27
+ const hasPks = primaryKeys.length > 0;
28
+ const pks = hasPks ? primaryKeys.map((pk) => `'${pk}'`).join(' | ') : null;
29
+
30
+ if (hasPks) {
31
+ code += `export type ${dbPrefix}New${singularRaw} = Omit<${singular}, ${pks}>;\n`;
32
+ }
33
+
34
+ if (isDifferent) {
35
+ code += `export type ${plural} = ${singular}[];\n`;
36
+ }
37
+
38
+ // Regardless
39
+ if (singular !== `${singular}Record`) {
40
+ code += `export type { ${singular} as ${singular}Record };\n`;
41
+ }
42
+ code += `export type ${singular}Records = ${singular}[];\n`;
43
+ if (hasPks) {
44
+ code += `export type ${dbPrefix}New${singularRaw}Record = Omit<${singular}, ${pks}>;\n`;
45
+ }
46
+
47
+ return code;
48
+ }
@@ -0,0 +1,59 @@
1
+ import type { Table } from 'harperdb';
2
+ import { isNullable } from './isNullable.ts';
3
+ import { mapType } from './mapType.ts';
4
+ import { singularize } from './singularize.ts';
5
+
6
+ /**
7
+ * Generates JSDoc types for a given HarperDB table.
8
+ *
9
+ * Example output:
10
+ * /**
11
+ * * @typedef {Object} Track
12
+ * * @property {string} id
13
+ * * @property {string} name
14
+ * * @property {any} [mp3]
15
+ * *\/
16
+ */
17
+ export function generateJSDoc(table: Table & { databaseName?: string }) {
18
+ const pluralRaw = table.tableName;
19
+ const singularRaw = singularize(pluralRaw);
20
+ const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : '';
21
+ const plural = `${dbPrefix}${pluralRaw}`;
22
+ const singular = `${dbPrefix}${singularRaw}`;
23
+ const isDifferent = plural !== singular;
24
+
25
+ let code = `\n/**\n * @typedef {Object} ${singular}\n`;
26
+ const primaryKeys: string[] = [];
27
+ for (const attribute of table.attributes || []) {
28
+ const type = mapType(attribute);
29
+ const primaryKey = !!attribute.isPrimaryKey;
30
+ const nullable = !primaryKey && isNullable(attribute);
31
+ code += ` * @property {${type}} ${nullable ? '[' : ''}${attribute.name}${nullable ? ']' : ''}\n`;
32
+ if (primaryKey) {
33
+ primaryKeys.push(attribute.name!);
34
+ }
35
+ }
36
+ code += ` */\n\n`;
37
+
38
+ const hasPks = primaryKeys.length > 0;
39
+ const pks = hasPks ? primaryKeys.map((pk) => `'${pk}'`).join(' | ') : null;
40
+
41
+ if (hasPks) {
42
+ code += `/** @typedef {Omit<${singular}, ${pks}>} ${dbPrefix}New${singularRaw} */\n`;
43
+ }
44
+
45
+ if (isDifferent) {
46
+ code += `/** @typedef {${singular}[]} ${plural} */\n`;
47
+ }
48
+
49
+ // Regardless
50
+ if (singular !== `${singular}Record`) {
51
+ code += `/** @typedef {${singular}} ${singular}Record */\n`;
52
+ }
53
+ code += `/** @typedef {${singular}[]} ${singular}Records */\n`;
54
+ if (hasPks) {
55
+ code += `/** @typedef {Omit<${singular}, ${pks}>} ${dbPrefix}New${singularRaw}Record */\n`;
56
+ }
57
+
58
+ return code;
59
+ }
@@ -0,0 +1,26 @@
1
+ import type { Table } from 'harperdb';
2
+ import { generateJSDoc } from './generateJSDoc.ts';
3
+ import { singularize } from './singularize.ts';
4
+ import type { TableMeta } from './tableMeta.ts';
5
+
6
+ export function generateJSDocFromTables(
7
+ tablesInput: (Table & { databaseName: string })[],
8
+ label: string = 'HarperDB schemas',
9
+ ) {
10
+ let jsCode = `/**
11
+ Generated from ${label}
12
+ Manual changes will be lost!
13
+ > harper dev .
14
+ */`;
15
+ const tables: TableMeta[] = [];
16
+
17
+ for (const table of tablesInput) {
18
+ jsCode += generateJSDoc(table);
19
+ const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : '';
20
+ const plural = `${dbPrefix}${table.tableName}`;
21
+ const singular = `${dbPrefix}${singularize(table.tableName)}`;
22
+ tables.push({ plural, singular, databaseName: table.databaseName });
23
+ }
24
+
25
+ return { jsCode, tables };
26
+ }
@@ -0,0 +1,26 @@
1
+ import type { Table } from 'harperdb';
2
+ import { generateInterface } from './generateInterface.ts';
3
+ import { singularize } from './singularize.ts';
4
+ import type { TableMeta } from './tableMeta.ts';
5
+
6
+ export function generateTSFromTables(
7
+ tablesInput: (Table & { databaseName: string })[],
8
+ label: string = 'HarperDB schemas',
9
+ ) {
10
+ let tsCode = `/**
11
+ Generated from ${label}
12
+ Manual changes will be lost!
13
+ > harper dev .
14
+ */`;
15
+ const tables: TableMeta[] = [];
16
+
17
+ for (const table of tablesInput) {
18
+ tsCode += generateInterface(table);
19
+ const dbPrefix = table.databaseName && table.databaseName !== 'data' ? `${table.databaseName}_` : '';
20
+ const plural = `${dbPrefix}${table.tableName}`;
21
+ const singular = `${dbPrefix}${singularize(table.tableName)}`;
22
+ tables.push({ plural, singular, databaseName: table.databaseName });
23
+ }
24
+
25
+ return { tsCode, tables };
26
+ }
@@ -0,0 +1,65 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getLogger } from './logger.ts';
4
+ import type { TableMeta } from './tableMeta.ts';
5
+
6
+ export function generateTablesDTS(globalTypesPath: string, schemaTypesPath: string, tables: TableMeta[]) {
7
+ let content = `/**
8
+ Generated from your schema files
9
+ Manual changes will be lost!
10
+ > harper dev .
11
+ */
12
+ `;
13
+ content += `import type { Table } from 'harperdb';\n`;
14
+ // Build a single import of all relevant types from schemaTypesPath
15
+ const namesToImport = new Set<string>();
16
+ for (const table of tables) {
17
+ namesToImport.add(table.singular);
18
+ }
19
+ if (namesToImport.size > 0) {
20
+ const fromPathRaw = path.relative(path.dirname(globalTypesPath), schemaTypesPath);
21
+ const fromPath = fromPathRaw.startsWith('.') ? fromPathRaw : './' + fromPathRaw;
22
+ content += `import type { ${Array.from(namesToImport).join(', ')} } from '${fromPath}';\n`;
23
+ }
24
+ content += '\n';
25
+
26
+ const dbMap = new Map<string, TableMeta[]>();
27
+ for (const table of tables) {
28
+ if (!dbMap.has(table.databaseName)) {
29
+ dbMap.set(table.databaseName, []);
30
+ }
31
+ dbMap.get(table.databaseName)!.push(table);
32
+ }
33
+
34
+ content += `declare module 'harperdb' {\n`;
35
+
36
+ // Export top-level tables for 'data' database
37
+ const dataTables = dbMap.get('data') || [];
38
+ content += `\texport const tables: {\n`;
39
+ for (const table of dataTables) {
40
+ content += `\t\t${table.plural}: { new(...args: any[]): Table<${table.singular}> };\n`;
41
+ }
42
+ content += `\t};\n\n`;
43
+
44
+ // Export namespaced databases
45
+ content += `\texport const databases: {\n`;
46
+ for (const [dbName, dbTables] of dbMap.entries()) {
47
+ content += `\t\t${dbName}: {\n`;
48
+ for (const table of dbTables) {
49
+ const pluralRaw = table.plural.startsWith(`${dbName}_`) ? table.plural.slice(dbName.length + 1) : table.plural;
50
+ content += `\t\t\t${pluralRaw}: { new(...args: any[]): Table<${table.singular}> };\n`;
51
+ }
52
+ content += `\t\t};\n`;
53
+ }
54
+ content += `\t};\n`;
55
+
56
+ content += `}\n`;
57
+ const outPath = globalTypesPath;
58
+ const dir = path.dirname(outPath);
59
+ if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
60
+ const existingContent = fs.existsSync(outPath) && fs.readFileSync(outPath, 'utf8');
61
+ if (existingContent !== content) {
62
+ fs.writeFileSync(outPath, content, 'utf8');
63
+ getLogger().debug?.(`Updated types in ${outPath}`);
64
+ }
65
+ }
@@ -0,0 +1,7 @@
1
+ import type { Attribute } from 'harperdb';
2
+
3
+ export function isNullable(attribute: Attribute) {
4
+ // Primary keys are always required
5
+ if (attribute.isPrimaryKey) { return false; }
6
+ return !!attribute.nullable || attribute.nullable === undefined;
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { Logger } from 'harperdb';
2
+
3
+ let logger: Logger | null = null;
4
+
5
+ export function setLogger(newLogger: Logger) {
6
+ logger = newLogger;
7
+ }
8
+
9
+ export function getLogger(): Logger {
10
+ return logger!;
11
+ }
@@ -0,0 +1,51 @@
1
+ import type { Attribute } from 'harperdb';
2
+ import { singularize } from './singularize.ts';
3
+
4
+ function mapObjectType(properties: Attribute[] | undefined): string {
5
+ if (!properties || properties.length === 0) { return 'Record<string, any>'; }
6
+ const fields = properties
7
+ .map((prop) => {
8
+ const optional = prop.isPrimaryKey ? false : !!prop.nullable;
9
+ return `${prop.name}${optional ? '?' : ''}: ${mapType(prop)};`;
10
+ })
11
+ .join(' ');
12
+ return `{ ${fields} }`;
13
+ }
14
+
15
+ export function mapType(attribute: Attribute): string {
16
+ const name = attribute?.type || 'Any';
17
+ switch (name) {
18
+ case 'String':
19
+ case 'ID':
20
+ return 'string';
21
+ case 'Int':
22
+ case 'Float':
23
+ case 'Long':
24
+ return 'number';
25
+ case 'BigInt':
26
+ return 'bigint';
27
+ case 'Boolean':
28
+ return 'boolean';
29
+ case 'Date':
30
+ return 'string';
31
+ case 'Bytes':
32
+ case 'Blob':
33
+ case 'Any':
34
+ return 'any';
35
+ case 'array':
36
+ case 'Array':
37
+ if (attribute.elements) {
38
+ return `${mapType(attribute.elements)}[]`;
39
+ }
40
+ return 'any[]';
41
+ case 'object':
42
+ case 'Object':
43
+ if (attribute.properties && attribute.properties.length) {
44
+ return mapObjectType(attribute.properties);
45
+ }
46
+ return 'Record<string, any>';
47
+ default:
48
+ // Fallback to using the type name (singularized) as an interface reference
49
+ return singularize(name);
50
+ }
51
+ }
@@ -0,0 +1,20 @@
1
+ import path from 'path';
2
+ import { collectTables } from './collectTables.ts';
3
+ import { generateJSDocFromTables } from './generateJSDocFromTables.ts';
4
+ import { generateTablesDTS } from './generateTablesDTS.ts';
5
+ import { generateTSFromTables } from './generateTS.ts';
6
+ import type { TableMeta } from './tableMeta.ts';
7
+ import { writeIfChanged } from './writeIfChanged.ts';
8
+
9
+ export function regenerateAll(globalTypes: string, schemaTypes: string, jsdoc?: string) {
10
+ const list = collectTables();
11
+
12
+ if (jsdoc) {
13
+ const { jsCode } = generateJSDocFromTables(list, 'HarperDB schema');
14
+ writeIfChanged(path.resolve(jsdoc), jsCode);
15
+ } else {
16
+ const { tsCode, tables } = generateTSFromTables(list, 'HarperDB schema');
17
+ writeIfChanged(path.resolve(schemaTypes), tsCode);
18
+ generateTablesDTS(path.resolve(globalTypes), path.resolve(schemaTypes), tables as TableMeta[]);
19
+ }
20
+ }
@@ -0,0 +1,61 @@
1
+ export function singularize(word: string): string {
2
+ const lower = word.toLowerCase();
3
+
4
+ // Nouns that are the same in singular and plural or should be treated as unchanged here
5
+ const noChange = new Set([
6
+ 'moose',
7
+ 'series',
8
+ 'species',
9
+ 'sheep',
10
+ 'fish',
11
+ 'deer',
12
+ 'news',
13
+ 'chassis',
14
+ 'aircraft',
15
+ 'bison',
16
+ 'salmon',
17
+ 'trout',
18
+ 'swine',
19
+ 'media',
20
+ ]);
21
+ if (noChange.has(lower)) {
22
+ return word;
23
+ }
24
+
25
+ // Irregular plurals not covered by suffix rules
26
+ const irregular: Record<string, string> = {
27
+ people: 'person',
28
+ men: 'man',
29
+ women: 'woman',
30
+ children: 'child',
31
+ geese: 'goose',
32
+ mice: 'mouse',
33
+ teeth: 'tooth',
34
+ feet: 'foot',
35
+ oxen: 'ox',
36
+ indices: 'index',
37
+ matrices: 'matrix',
38
+ vertices: 'vertex',
39
+ };
40
+ if (Object.prototype.hasOwnProperty.call(irregular, lower)) {
41
+ const singular = irregular[lower];
42
+ // Preserve capitalization of the first letter
43
+ if (word[0] === word[0].toUpperCase()) {
44
+ return singular.charAt(0).toUpperCase() + singular.slice(1);
45
+ }
46
+ return singular;
47
+ }
48
+
49
+ if (word.endsWith('ies')) {
50
+ return word.slice(0, -3) + 'y';
51
+ }
52
+ if (word.endsWith('es')) {
53
+ if (word.endsWith('xes') || word.endsWith('ses') || word.endsWith('ches') || word.endsWith('shes')) {
54
+ return word.slice(0, -2);
55
+ }
56
+ }
57
+ if (word.endsWith('s') && !word.endsWith('ss')) {
58
+ return word.slice(0, -1);
59
+ }
60
+ return word;
61
+ }
package/utils/sleep.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function sleep(ms: number) {
2
+ return new Promise(resolve => setTimeout(resolve, ms));
3
+ }
@@ -0,0 +1,5 @@
1
+ export interface TableMeta {
2
+ singular: string;
3
+ plural: string;
4
+ databaseName: string;
5
+ }
@@ -0,0 +1,13 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getLogger } from './logger.ts';
4
+
5
+ export function writeIfChanged(filePath: string, content: string) {
6
+ const dir = path.dirname(filePath);
7
+ if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
8
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : undefined;
9
+ if (existing !== content) {
10
+ fs.writeFileSync(filePath, content, 'utf8');
11
+ getLogger().debug?.(`Updated types in ${filePath}`);
12
+ }
13
+ }