@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 +107 -0
- package/config.yaml +1 -0
- package/extensionModule.ts +58 -0
- package/package.json +40 -0
- package/utils/collectTables.ts +16 -0
- package/utils/generateInterface.ts +48 -0
- package/utils/generateJSDoc.ts +59 -0
- package/utils/generateJSDocFromTables.ts +26 -0
- package/utils/generateTS.ts +26 -0
- package/utils/generateTablesDTS.ts +65 -0
- package/utils/isNullable.ts +7 -0
- package/utils/logger.ts +11 -0
- package/utils/mapType.ts +51 -0
- package/utils/regenerateAll.ts +20 -0
- package/utils/singularize.ts +61 -0
- package/utils/sleep.ts +3 -0
- package/utils/tableMeta.ts +5 -0
- package/utils/writeIfChanged.ts +13 -0
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
|
+
}
|
package/utils/logger.ts
ADDED
package/utils/mapType.ts
ADDED
|
@@ -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,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
|
+
}
|