@gridfox/codegen 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.
- package/.env.example +3 -0
- package/README.md +1152 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +394 -0
- package/dist/cli/prompt.d.ts +4 -0
- package/dist/cli/prompt.js +89 -0
- package/dist/config/loadConfig.d.ts +2 -0
- package/dist/config/loadConfig.js +49 -0
- package/dist/config/schema.d.ts +21 -0
- package/dist/config/schema.js +17 -0
- package/dist/emit/formatter.d.ts +1 -0
- package/dist/emit/formatter.js +2 -0
- package/dist/emit/writer.d.ts +7 -0
- package/dist/emit/writer.js +37 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.js +53 -0
- package/dist/generators/generateIndexFile.d.ts +2 -0
- package/dist/generators/generateIndexFile.js +12 -0
- package/dist/generators/generateRegistryFile.d.ts +2 -0
- package/dist/generators/generateRegistryFile.js +7 -0
- package/dist/generators/generateSdkClientFile.d.ts +2 -0
- package/dist/generators/generateSdkClientFile.js +46 -0
- package/dist/generators/generateSharedTypes.d.ts +1 -0
- package/dist/generators/generateSharedTypes.js +4 -0
- package/dist/generators/generateTableModule.d.ts +2 -0
- package/dist/generators/generateTableModule.js +49 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/input/apiTransport.d.ts +6 -0
- package/dist/input/apiTransport.js +21 -0
- package/dist/input/parseTablesPayload.d.ts +21 -0
- package/dist/input/parseTablesPayload.js +71 -0
- package/dist/input/readApiInput.d.ts +9 -0
- package/dist/input/readApiInput.js +17 -0
- package/dist/input/readInput.d.ts +21 -0
- package/dist/input/readInput.js +14 -0
- package/dist/model/internalTypes.d.ts +60 -0
- package/dist/model/internalTypes.js +1 -0
- package/dist/model/normalizeTables.d.ts +6 -0
- package/dist/model/normalizeTables.js +68 -0
- package/dist/model/zodSchemas.d.ts +120 -0
- package/dist/model/zodSchemas.js +47 -0
- package/dist/naming/fieldAliases.d.ts +1 -0
- package/dist/naming/fieldAliases.js +3 -0
- package/dist/naming/identifiers.d.ts +1 -0
- package/dist/naming/identifiers.js +11 -0
- package/dist/naming/reservedWords.d.ts +1 -0
- package/dist/naming/reservedWords.js +13 -0
- package/dist/naming/tableNames.d.ts +1 -0
- package/dist/naming/tableNames.js +3 -0
- package/dist/typing/mapFieldType.d.ts +8 -0
- package/dist/typing/mapFieldType.js +95 -0
- package/dist/typing/writability.d.ts +1 -0
- package/dist/typing/writability.js +2 -0
- package/dist/utils/sort.d.ts +11 -0
- package/dist/utils/sort.js +5 -0
- package/dist/validate/crudPlan.d.ts +23 -0
- package/dist/validate/crudPlan.js +189 -0
- package/dist/validate/renderCrudTest.d.ts +2 -0
- package/dist/validate/renderCrudTest.js +180 -0
- package/package.json +57 -0
package/dist/generate.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { normalizeTables } from './model/normalizeTables.js';
|
|
2
|
+
import { generateTableModule } from './generators/generateTableModule.js';
|
|
3
|
+
import { generateSharedTypes } from './generators/generateSharedTypes.js';
|
|
4
|
+
import { generateRegistryFile } from './generators/generateRegistryFile.js';
|
|
5
|
+
import { generateIndexFile } from './generators/generateIndexFile.js';
|
|
6
|
+
import { generateSdkClientFile } from './generators/generateSdkClientFile.js';
|
|
7
|
+
import { diffGeneratedFiles, writeGeneratedFiles } from './emit/writer.js';
|
|
8
|
+
import { formatTypeScript } from './emit/formatter.js';
|
|
9
|
+
import { sortedKeys } from './utils/sort.js';
|
|
10
|
+
const applyFilters = (tables, config) => {
|
|
11
|
+
let filtered = [...tables];
|
|
12
|
+
if (config.includeTables?.length) {
|
|
13
|
+
const include = new Set(config.includeTables);
|
|
14
|
+
filtered = filtered.filter((t) => include.has(t.name));
|
|
15
|
+
}
|
|
16
|
+
if (config.excludeTables?.length) {
|
|
17
|
+
const exclude = new Set(config.excludeTables);
|
|
18
|
+
filtered = filtered.filter((t) => !exclude.has(t.name));
|
|
19
|
+
}
|
|
20
|
+
return filtered;
|
|
21
|
+
};
|
|
22
|
+
export const buildGeneratedFiles = async (tables, config) => {
|
|
23
|
+
const emitClient = config.emitClient === true || config.emitSdkClient === true;
|
|
24
|
+
const normalized = normalizeTables(applyFilters(tables, config), { multiSelectMode: config.multiSelectMode });
|
|
25
|
+
const files = {};
|
|
26
|
+
files['shared.ts'] = generateSharedTypes();
|
|
27
|
+
for (const table of normalized) {
|
|
28
|
+
files[`${table.symbolName}.ts`] = generateTableModule(table, config);
|
|
29
|
+
}
|
|
30
|
+
if (config.emitRegistry ?? true) {
|
|
31
|
+
files['tables.ts'] = generateRegistryFile(normalized);
|
|
32
|
+
}
|
|
33
|
+
if (emitClient) {
|
|
34
|
+
files['client.ts'] = generateSdkClientFile(normalized);
|
|
35
|
+
}
|
|
36
|
+
files['index.ts'] = generateIndexFile(normalized, config.emitRegistry ?? true, emitClient);
|
|
37
|
+
if (config.format ?? true) {
|
|
38
|
+
for (const key of sortedKeys(files)) {
|
|
39
|
+
files[key] = await formatTypeScript(files[key]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return files;
|
|
43
|
+
};
|
|
44
|
+
export const planGeneration = async (tables, config) => {
|
|
45
|
+
const files = await buildGeneratedFiles(tables, config);
|
|
46
|
+
const diff = await diffGeneratedFiles(config.output, files);
|
|
47
|
+
return { files, diff };
|
|
48
|
+
};
|
|
49
|
+
export const generateFromTables = async (tables, config) => {
|
|
50
|
+
const files = await buildGeneratedFiles(tables, config);
|
|
51
|
+
await writeGeneratedFiles(config.output, files);
|
|
52
|
+
return files;
|
|
53
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sortByTableName } from '../utils/sort.js';
|
|
2
|
+
export const generateIndexFile = (tables, emitRegistry, emitClient) => {
|
|
3
|
+
const tableExports = sortByTableName(tables)
|
|
4
|
+
.map((t) => `export * from './${t.symbolName}.js'`)
|
|
5
|
+
.join('\n');
|
|
6
|
+
const base = ["export * from './shared.js'", tableExports];
|
|
7
|
+
if (emitRegistry)
|
|
8
|
+
base.push("export * from './tables.js'");
|
|
9
|
+
if (emitClient)
|
|
10
|
+
base.push("export * from './client.js'");
|
|
11
|
+
return `${base.filter(Boolean).join('\n')}\n`;
|
|
12
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sortByTableName } from '../utils/sort.js';
|
|
2
|
+
export const generateRegistryFile = (tables) => {
|
|
3
|
+
const sortedTables = sortByTableName(tables);
|
|
4
|
+
const imports = sortedTables.map((t) => `import { ${t.symbolName} } from './${t.symbolName}.js'`).join('\n');
|
|
5
|
+
const entries = sortedTables.map((t) => ` ${t.symbolName}`).join(',\n');
|
|
6
|
+
return `${imports}\n\nexport const gridfoxTables = [\n${entries}\n] as const\n`;
|
|
7
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { camelCase } from 'change-case';
|
|
2
|
+
import { safeIdentifier } from '../naming/reservedWords.js';
|
|
3
|
+
import { sortByTableName } from '../utils/sort.js';
|
|
4
|
+
const namespaceNameFor = (table) => safeIdentifier(camelCase(table.symbolName));
|
|
5
|
+
const namingFor = (table) => ({
|
|
6
|
+
namespaceName: namespaceNameFor(table),
|
|
7
|
+
namespaceType: `${table.symbolName}Client`,
|
|
8
|
+
listMany: `list${table.symbolName}`,
|
|
9
|
+
getOne: `get${table.typeName}`,
|
|
10
|
+
createOne: `create${table.typeName}`,
|
|
11
|
+
updateOne: `update${table.typeName}`,
|
|
12
|
+
deleteOne: `delete${table.typeName}`
|
|
13
|
+
});
|
|
14
|
+
const moduleImport = (table) => `import { ${table.symbolName}, type ${table.typeName}CreateInput, type ${table.typeName}Record, type ${table.typeName}UpdateInput } from './${table.symbolName}.js'`;
|
|
15
|
+
const namespaceType = (table) => {
|
|
16
|
+
const names = namingFor(table);
|
|
17
|
+
return `export interface ${names.namespaceType} {\n list(): Promise<${table.typeName}Record[]>\n get(recordId: string): Promise<${table.typeName}Record>\n create(fields: ${table.typeName}CreateInput): Promise<${table.typeName}Record>\n update(recordId: string, fields: ${table.typeName}UpdateInput): Promise<${table.typeName}Record>\n delete(recordId: string): Promise<void>\n ${names.listMany}(): Promise<${table.typeName}Record[]>\n ${names.getOne}(recordId: string): Promise<${table.typeName}Record>\n ${names.createOne}(fields: ${table.typeName}CreateInput): Promise<${table.typeName}Record>\n ${names.updateOne}(recordId: string, fields: ${table.typeName}UpdateInput): Promise<${table.typeName}Record>\n ${names.deleteOne}(recordId: string): Promise<void>\n}`;
|
|
18
|
+
};
|
|
19
|
+
const projectClientType = (tables) => `export interface GridfoxProjectClient {\n${sortByTableName(tables)
|
|
20
|
+
.map((table) => {
|
|
21
|
+
const names = namingFor(table);
|
|
22
|
+
return ` ${names.namespaceName}: ${names.namespaceType}`;
|
|
23
|
+
})
|
|
24
|
+
.join('\n')}\n}`;
|
|
25
|
+
const namespaceFactory = (table) => {
|
|
26
|
+
const names = namingFor(table);
|
|
27
|
+
return `const create${table.symbolName}Client = (tableClient: {\n list(): Promise<${table.typeName}Record[]>\n get(recordId: string): Promise<${table.typeName}Record>\n create(fields: ${table.typeName}CreateInput): Promise<${table.typeName}Record>\n update(recordId: string, fields: ${table.typeName}UpdateInput): Promise<${table.typeName}Record>\n delete(recordId: string): Promise<void>\n}): ${names.namespaceType} => ({\n list: () => tableClient.list(),\n get: (recordId) => tableClient.get(recordId),\n create: (fields) => tableClient.create(fields),\n update: (recordId, fields) => tableClient.update(recordId, fields),\n delete: (recordId) => tableClient.delete(recordId),\n ${names.listMany}: () => tableClient.list(),\n ${names.getOne}: (recordId) => tableClient.get(recordId),\n ${names.createOne}: (fields) => tableClient.create(fields),\n ${names.updateOne}: (recordId, fields) => tableClient.update(recordId, fields),\n ${names.deleteOne}: (recordId) => tableClient.delete(recordId)\n})`;
|
|
28
|
+
};
|
|
29
|
+
const projectClientFactory = (tables) => `export const createProjectClient = (options: GridfoxClientOptions): GridfoxProjectClient => {\n const client = createGridfoxClient(options)\n${sortByTableName(tables)
|
|
30
|
+
.map((table) => {
|
|
31
|
+
const names = namingFor(table);
|
|
32
|
+
return ` const ${names.namespaceName}Table = client.table<${table.typeName}Record, ${table.typeName}CreateInput, ${table.typeName}UpdateInput>(${table.symbolName})\n const ${names.namespaceName} = create${table.symbolName}Client(${names.namespaceName}Table)`;
|
|
33
|
+
})
|
|
34
|
+
.join('\n')}\n\n return {\n${sortByTableName(tables)
|
|
35
|
+
.map((table) => {
|
|
36
|
+
const names = namingFor(table);
|
|
37
|
+
return ` ${names.namespaceName}`;
|
|
38
|
+
})
|
|
39
|
+
.join(',\n')}\n }\n}`;
|
|
40
|
+
export const generateSdkClientFile = (tables) => {
|
|
41
|
+
const sortedTables = sortByTableName(tables);
|
|
42
|
+
const imports = sortedTables.map(moduleImport).join('\n');
|
|
43
|
+
const namespaceTypes = sortedTables.map(namespaceType).join('\n\n');
|
|
44
|
+
const namespaceFactories = sortedTables.map(namespaceFactory).join('\n\n');
|
|
45
|
+
return `import { createGridfoxClient, type GridfoxClientOptions } from '@gridfox/sdk'\n${imports ? `\n${imports}` : ''}\n\n${namespaceTypes}\n\n${projectClientType(sortedTables)}\n\n${namespaceFactories}\n\n${projectClientFactory(sortedTables)}\n`;
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const generateSharedTypes: () => string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { pascalCase } from 'change-case';
|
|
2
|
+
import { sortByFieldName } from '../utils/sort.js';
|
|
3
|
+
const listUnionName = (table, field) => `${table.typeName}${pascalCase(field.alias)}`;
|
|
4
|
+
const quoted = (s) => JSON.stringify(s);
|
|
5
|
+
const listUnions = (table, config) => {
|
|
6
|
+
const multiSelectMode = config.multiSelectMode ?? 'union';
|
|
7
|
+
const sortedFields = sortByFieldName(table.fields);
|
|
8
|
+
return sortedFields
|
|
9
|
+
.filter((f) => (f.kind === 'list' || (f.kind === 'multiSelectList' && multiSelectMode === 'union')) && f.options?.length)
|
|
10
|
+
.map((f) => `export type ${listUnionName(table, f)} = ${f.options.map(quoted).join(' | ')}`)
|
|
11
|
+
.join('\n');
|
|
12
|
+
};
|
|
13
|
+
const fieldsObject = (table) => sortByFieldName(table.fields)
|
|
14
|
+
.map((f) => ` ${f.alias}: ${quoted(f.fieldName)}`)
|
|
15
|
+
.join(',\n');
|
|
16
|
+
const readFieldEntries = (table) => sortByFieldName(table.fields)
|
|
17
|
+
.map((f) => ` ${quoted(f.fieldName)}?: ${f.tsReadType}`)
|
|
18
|
+
.join('\n');
|
|
19
|
+
const writableFields = (table) => sortByFieldName(table.fields.filter((f) => f.writable));
|
|
20
|
+
const writeFieldEntries = (table) => writableFields(table)
|
|
21
|
+
.map((f) => {
|
|
22
|
+
const optional = f.required ? '' : '?';
|
|
23
|
+
return ` ${quoted(f.fieldName)}${optional}: ${f.tsWriteType}`;
|
|
24
|
+
})
|
|
25
|
+
.join('\n');
|
|
26
|
+
const metadataEntries = (table) => {
|
|
27
|
+
const sortedFields = sortByFieldName(table.fields);
|
|
28
|
+
return sortedFields
|
|
29
|
+
.map((f) => {
|
|
30
|
+
const rel = f.relatedTableName ? `, relatedTableName: ${quoted(f.relatedTableName)}` : '';
|
|
31
|
+
const opts = f.options?.length ? `, options: [${f.options.map(quoted).join(', ')}]` : '';
|
|
32
|
+
return ` ${f.alias}: { name: ${quoted(f.fieldName)}, kind: ${quoted(f.kind)}, required: ${f.required}, unique: ${f.unique}${rel}${opts} }`;
|
|
33
|
+
})
|
|
34
|
+
.join(',\n');
|
|
35
|
+
};
|
|
36
|
+
export const generateTableModule = (table, config) => {
|
|
37
|
+
const writable = writableFields(table);
|
|
38
|
+
const readonly = sortByFieldName(table.fields.filter((f) => !f.writable));
|
|
39
|
+
const reverseAliasMap = config.emitReverseAliasMap
|
|
40
|
+
? `\nexport const ${table.symbolName}ReverseAliases = {\n${sortByFieldName(table.fields)
|
|
41
|
+
.map((f) => ` ${quoted(f.fieldName)}: ${quoted(f.alias)}`)
|
|
42
|
+
.join(',\n')}\n} as const\n`
|
|
43
|
+
: '';
|
|
44
|
+
const metadata = config.emitMetadata
|
|
45
|
+
? `\nexport const ${table.symbolName}Metadata = {\n tableName: ${quoted(table.tableName)},\n singularName: ${quoted(table.singularName)},\n referenceFieldName: ${quoted(table.referenceFieldName)},\n fields: {\n${metadataEntries(table)}\n }\n} as const\n`
|
|
46
|
+
: '';
|
|
47
|
+
const unions = listUnions(table, config);
|
|
48
|
+
return `import type { GridfoxFileValue, GridfoxImageValue, GridfoxLinkedValue } from './shared.js'\n\nexport const ${table.symbolName} = {\n tableName: ${quoted(table.tableName)},\n singularName: ${quoted(table.singularName)},\n referenceFieldName: ${quoted(table.referenceFieldName)},\n fields: {\n${fieldsObject(table)}\n }\n} as const\n\n${unions ? `${unions}\n\n` : ''}export interface ${table.typeName}Record {\n id?: string\n fields: {\n${readFieldEntries(table)}\n }\n}\n\nexport type ${table.typeName}CreateInput = {\n${writeFieldEntries(table)}\n}\n\nexport type ${table.typeName}UpdateInput = Partial<${table.typeName}CreateInput>\n\nexport const ${table.symbolName}WritableFields = [${writable.map((f) => quoted(f.fieldName)).join(', ')}] as const\n\nexport const ${table.symbolName}ReadonlyFields = [${readonly.map((f) => quoted(f.fieldName)).join(', ')}] as const\n${metadata}${reverseAliasMap}`;
|
|
49
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { generateFromTables } from './generate.js';
|
|
2
|
+
export { buildGeneratedFiles, planGeneration } from './generate.js';
|
|
3
|
+
export { normalizeTables } from './model/normalizeTables.js';
|
|
4
|
+
export { loadConfig } from './config/loadConfig.js';
|
|
5
|
+
export { readTablesInput } from './input/readInput.js';
|
|
6
|
+
export { readTablesFromApi, buildTablesApiUrl, type GridfoxApiInputConfig } from './input/readApiInput.js';
|
|
7
|
+
export { buildHeuristicPlan } from './validate/crudPlan.js';
|
|
8
|
+
export { buildCrudTestSource } from './validate/renderCrudTest.js';
|
|
9
|
+
export type { GridfoxCodegenConfig, RawField, RawTable, NormalizedField, NormalizedTable } from './model/internalTypes.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { generateFromTables } from './generate.js';
|
|
2
|
+
export { buildGeneratedFiles, planGeneration } from './generate.js';
|
|
3
|
+
export { normalizeTables } from './model/normalizeTables.js';
|
|
4
|
+
export { loadConfig } from './config/loadConfig.js';
|
|
5
|
+
export { readTablesInput } from './input/readInput.js';
|
|
6
|
+
export { readTablesFromApi, buildTablesApiUrl } from './input/readApiInput.js';
|
|
7
|
+
export { buildHeuristicPlan } from './validate/crudPlan.js';
|
|
8
|
+
export { buildCrudTestSource } from './validate/renderCrudTest.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ApiTransport {
|
|
2
|
+
getJson(url: string, headers: Record<string, string>, timeoutMs: number): Promise<unknown>;
|
|
3
|
+
}
|
|
4
|
+
export declare class FetchApiTransport implements ApiTransport {
|
|
5
|
+
getJson(url: string, headers: Record<string, string>, timeoutMs: number): Promise<unknown>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class FetchApiTransport {
|
|
2
|
+
async getJson(url, headers, timeoutMs) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch(url, {
|
|
7
|
+
method: 'GET',
|
|
8
|
+
headers,
|
|
9
|
+
signal: controller.signal
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
const body = await response.text();
|
|
13
|
+
throw new Error(`HTTP ${response.status}: ${body.slice(0, 300)}`);
|
|
14
|
+
}
|
|
15
|
+
return (await response.json());
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const parseTablesPayload: (parsed: unknown, sourceLabel?: string) => {
|
|
2
|
+
name: string;
|
|
3
|
+
singularName: string;
|
|
4
|
+
referenceFieldName: string;
|
|
5
|
+
fields: {
|
|
6
|
+
name: string;
|
|
7
|
+
type: "number" | "text" | "email" | "textArea" | "richText" | "user" | "money" | "percentage" | "formula" | "autoCounter" | "checkbox" | "date" | "dateTime" | "list" | "multiSelectList" | "parent" | "child" | "manyToMany" | "file" | "image";
|
|
8
|
+
isRequired?: boolean | undefined;
|
|
9
|
+
isUnique?: boolean | undefined;
|
|
10
|
+
properties?: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
items?: {
|
|
13
|
+
value: string;
|
|
14
|
+
color?: string | null | undefined;
|
|
15
|
+
}[] | undefined;
|
|
16
|
+
relatedTableName?: string | undefined;
|
|
17
|
+
formulaType?: string | undefined;
|
|
18
|
+
currency?: string | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}[];
|
|
21
|
+
}[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { GRIDFOX_FIELD_TYPES, GridfoxTablesSchema } from '../model/zodSchemas.js';
|
|
2
|
+
const knownFieldTypes = new Set(GRIDFOX_FIELD_TYPES);
|
|
3
|
+
const tableNameAt = (parsed, tableIndex) => {
|
|
4
|
+
if (!Array.isArray(parsed))
|
|
5
|
+
return `#${tableIndex}`;
|
|
6
|
+
const value = parsed[tableIndex];
|
|
7
|
+
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
|
8
|
+
return value.name;
|
|
9
|
+
}
|
|
10
|
+
return `#${tableIndex}`;
|
|
11
|
+
};
|
|
12
|
+
const fieldNameAt = (parsed, tableIndex, fieldIndex) => {
|
|
13
|
+
if (!Array.isArray(parsed))
|
|
14
|
+
return `#${fieldIndex}`;
|
|
15
|
+
const table = parsed[tableIndex];
|
|
16
|
+
if (!table || typeof table !== 'object' || !Array.isArray(table.fields)) {
|
|
17
|
+
return `#${fieldIndex}`;
|
|
18
|
+
}
|
|
19
|
+
const field = table.fields[fieldIndex];
|
|
20
|
+
if (field && typeof field === 'object' && typeof field.name === 'string') {
|
|
21
|
+
const name = field.name.trim();
|
|
22
|
+
return name || `#${fieldIndex}`;
|
|
23
|
+
}
|
|
24
|
+
return `#${fieldIndex}`;
|
|
25
|
+
};
|
|
26
|
+
const findUnknownFieldTypeErrors = (parsed) => {
|
|
27
|
+
if (!Array.isArray(parsed)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const errors = [];
|
|
31
|
+
for (let tableIndex = 0; tableIndex < parsed.length; tableIndex += 1) {
|
|
32
|
+
const table = parsed[tableIndex];
|
|
33
|
+
if (!table || typeof table !== 'object' || !Array.isArray(table.fields)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (let fieldIndex = 0; fieldIndex < table.fields.length; fieldIndex += 1) {
|
|
37
|
+
const field = table.fields[fieldIndex];
|
|
38
|
+
if (!field || typeof field !== 'object') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof field.type === 'string' && !knownFieldTypes.has(field.type)) {
|
|
42
|
+
const tableName = tableNameAt(parsed, tableIndex);
|
|
43
|
+
const fieldName = fieldNameAt(parsed, tableIndex, fieldIndex);
|
|
44
|
+
errors.push(`Unknown field type "${field.type}" at table "${tableName}", field "${fieldName}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return errors;
|
|
49
|
+
};
|
|
50
|
+
const formatIssue = (parsed, issue) => {
|
|
51
|
+
const [tableIndex, segment, fieldIndex] = issue.path;
|
|
52
|
+
if (typeof tableIndex === 'number' && segment === 'fields' && typeof fieldIndex === 'number') {
|
|
53
|
+
return `Table "${tableNameAt(parsed, tableIndex)}", field "${fieldNameAt(parsed, tableIndex, fieldIndex)}": ${issue.message}`;
|
|
54
|
+
}
|
|
55
|
+
if (typeof tableIndex === 'number') {
|
|
56
|
+
return `Table "${tableNameAt(parsed, tableIndex)}": ${issue.message}`;
|
|
57
|
+
}
|
|
58
|
+
return issue.message;
|
|
59
|
+
};
|
|
60
|
+
export const parseTablesPayload = (parsed, sourceLabel = 'tables input') => {
|
|
61
|
+
const unknownTypeErrors = findUnknownFieldTypeErrors(parsed);
|
|
62
|
+
if (unknownTypeErrors.length > 0) {
|
|
63
|
+
throw new Error([`Invalid ${sourceLabel}:`, ...unknownTypeErrors].join('\n'));
|
|
64
|
+
}
|
|
65
|
+
const result = GridfoxTablesSchema.safeParse(parsed);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
const details = result.error.issues.map((issue) => `- ${formatIssue(parsed, issue)}`);
|
|
68
|
+
throw new Error([`Invalid ${sourceLabel}:`, ...details].join('\n'));
|
|
69
|
+
}
|
|
70
|
+
return result.data;
|
|
71
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RawTable } from '../model/internalTypes.js';
|
|
2
|
+
import type { ApiTransport } from './apiTransport.js';
|
|
3
|
+
export interface GridfoxApiInputConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiBaseUrl?: string;
|
|
6
|
+
apiTimeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const buildTablesApiUrl: (config: GridfoxApiInputConfig) => string;
|
|
9
|
+
export declare const readTablesFromApi: (config: GridfoxApiInputConfig, transport?: ApiTransport) => Promise<RawTable[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FetchApiTransport } from './apiTransport.js';
|
|
2
|
+
import { parseTablesPayload } from './parseTablesPayload.js';
|
|
3
|
+
const defaultBaseUrl = 'https://api.gridfox.com';
|
|
4
|
+
const defaultTimeoutMs = 10000;
|
|
5
|
+
export const buildTablesApiUrl = (config) => {
|
|
6
|
+
const base = config.apiBaseUrl ?? defaultBaseUrl;
|
|
7
|
+
return new URL('/tables', base).toString();
|
|
8
|
+
};
|
|
9
|
+
export const readTablesFromApi = async (config, transport = new FetchApiTransport()) => {
|
|
10
|
+
const url = buildTablesApiUrl(config);
|
|
11
|
+
const timeoutMs = config.apiTimeoutMs ?? defaultTimeoutMs;
|
|
12
|
+
const payload = await transport.getJson(url, {
|
|
13
|
+
accept: 'application/json',
|
|
14
|
+
'gridfox-api-key': config.apiKey
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
return parseTablesPayload(payload, `tables API response from "${url}"`);
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const readTablesInput: (inputPath: string) => Promise<{
|
|
2
|
+
name: string;
|
|
3
|
+
singularName: string;
|
|
4
|
+
referenceFieldName: string;
|
|
5
|
+
fields: {
|
|
6
|
+
name: string;
|
|
7
|
+
type: "number" | "text" | "email" | "textArea" | "richText" | "user" | "money" | "percentage" | "formula" | "autoCounter" | "checkbox" | "date" | "dateTime" | "list" | "multiSelectList" | "parent" | "child" | "manyToMany" | "file" | "image";
|
|
8
|
+
isRequired?: boolean | undefined;
|
|
9
|
+
isUnique?: boolean | undefined;
|
|
10
|
+
properties?: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
items?: {
|
|
13
|
+
value: string;
|
|
14
|
+
color?: string | null | undefined;
|
|
15
|
+
}[] | undefined;
|
|
16
|
+
relatedTableName?: string | undefined;
|
|
17
|
+
formulaType?: string | undefined;
|
|
18
|
+
currency?: string | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}[];
|
|
21
|
+
}[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseTablesPayload } from './parseTablesPayload.js';
|
|
3
|
+
export const readTablesInput = async (inputPath) => {
|
|
4
|
+
const raw = await readFile(inputPath, 'utf8');
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : 'unknown parse error';
|
|
11
|
+
throw new Error(`Invalid JSON in "${inputPath}": ${message}`);
|
|
12
|
+
}
|
|
13
|
+
return parseTablesPayload(parsed, `tables input from "${inputPath}"`);
|
|
14
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type GridfoxFieldType = 'text' | 'email' | 'textArea' | 'richText' | 'user' | 'number' | 'money' | 'percentage' | 'formula' | 'autoCounter' | 'checkbox' | 'date' | 'dateTime' | 'list' | 'multiSelectList' | 'parent' | 'child' | 'manyToMany' | 'file' | 'image';
|
|
2
|
+
export interface RawField {
|
|
3
|
+
name: string;
|
|
4
|
+
type: GridfoxFieldType;
|
|
5
|
+
isRequired?: boolean;
|
|
6
|
+
isUnique?: boolean;
|
|
7
|
+
properties?: {
|
|
8
|
+
items?: Array<{
|
|
9
|
+
value: string;
|
|
10
|
+
color?: string | null;
|
|
11
|
+
}>;
|
|
12
|
+
relatedTableName?: string;
|
|
13
|
+
formulaType?: string;
|
|
14
|
+
currency?: string;
|
|
15
|
+
[k: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface RawTable {
|
|
19
|
+
name: string;
|
|
20
|
+
singularName: string;
|
|
21
|
+
referenceFieldName: string;
|
|
22
|
+
fields: RawField[];
|
|
23
|
+
}
|
|
24
|
+
export interface NormalizedField {
|
|
25
|
+
fieldName: string;
|
|
26
|
+
alias: string;
|
|
27
|
+
kind: GridfoxFieldType;
|
|
28
|
+
tsReadType: string;
|
|
29
|
+
tsWriteType?: string;
|
|
30
|
+
writable: boolean;
|
|
31
|
+
required: boolean;
|
|
32
|
+
unique: boolean;
|
|
33
|
+
relatedTableName?: string;
|
|
34
|
+
options?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface NormalizedTable {
|
|
37
|
+
tableName: string;
|
|
38
|
+
singularName: string;
|
|
39
|
+
symbolName: string;
|
|
40
|
+
typeName: string;
|
|
41
|
+
referenceFieldName: string;
|
|
42
|
+
fields: NormalizedField[];
|
|
43
|
+
}
|
|
44
|
+
export interface GridfoxCodegenConfig {
|
|
45
|
+
input?: string;
|
|
46
|
+
output: string;
|
|
47
|
+
includeTables?: string[];
|
|
48
|
+
excludeTables?: string[];
|
|
49
|
+
apiKey?: string;
|
|
50
|
+
apiBaseUrl?: string;
|
|
51
|
+
apiTimeoutMs?: number;
|
|
52
|
+
multiSelectMode?: 'union' | 'stringArray';
|
|
53
|
+
emitClient?: boolean;
|
|
54
|
+
emitRegistry?: boolean;
|
|
55
|
+
/** @deprecated use emitClient */
|
|
56
|
+
emitSdkClient?: boolean;
|
|
57
|
+
emitReverseAliasMap?: boolean;
|
|
58
|
+
emitMetadata?: boolean;
|
|
59
|
+
format?: boolean;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NormalizedTable, RawTable } from './internalTypes.js';
|
|
2
|
+
interface NormalizeOptions {
|
|
3
|
+
multiSelectMode?: 'union' | 'stringArray';
|
|
4
|
+
}
|
|
5
|
+
export declare const normalizeTables: (tables: RawTable[], options?: NormalizeOptions) => NormalizedTable[];
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { pascalCase } from 'change-case';
|
|
2
|
+
import { normalizeFieldAlias } from '../naming/fieldAliases.js';
|
|
3
|
+
import { ensureIdentifier } from '../naming/identifiers.js';
|
|
4
|
+
import { normalizeTableSymbol } from '../naming/tableNames.js';
|
|
5
|
+
import { mapReadType, mapWriteType } from '../typing/mapFieldType.js';
|
|
6
|
+
import { isWritableFieldType } from '../typing/writability.js';
|
|
7
|
+
import { sortByName } from '../utils/sort.js';
|
|
8
|
+
const normalizeField = (field, options) => {
|
|
9
|
+
const writable = isWritableFieldType(field.type);
|
|
10
|
+
const listOptions = field.properties?.items?.map((item) => item.value);
|
|
11
|
+
return {
|
|
12
|
+
fieldName: field.name,
|
|
13
|
+
alias: normalizeFieldAlias(field.name),
|
|
14
|
+
kind: field.type,
|
|
15
|
+
tsReadType: mapReadType(field, { multiSelectMode: options.multiSelectMode }),
|
|
16
|
+
tsWriteType: writable ? mapWriteType(field, { multiSelectMode: options.multiSelectMode }) : undefined,
|
|
17
|
+
writable,
|
|
18
|
+
required: Boolean(field.isRequired),
|
|
19
|
+
unique: Boolean(field.isUnique),
|
|
20
|
+
relatedTableName: field.properties?.relatedTableName,
|
|
21
|
+
options: listOptions
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const ensureNoCollisions = (tableName, fields) => {
|
|
25
|
+
const seen = new Map();
|
|
26
|
+
for (const field of fields) {
|
|
27
|
+
const prev = seen.get(field.alias);
|
|
28
|
+
if (prev) {
|
|
29
|
+
throw new Error(`Alias collision in table "${tableName}": fields "${prev}" and "${field.fieldName}" both normalize to "${field.alias}"`);
|
|
30
|
+
}
|
|
31
|
+
seen.set(field.alias, field.fieldName);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const ensureNoTableIdentifierCollisions = (tables) => {
|
|
35
|
+
const symbols = new Map();
|
|
36
|
+
const types = new Map();
|
|
37
|
+
for (const table of tables) {
|
|
38
|
+
const prevSymbol = symbols.get(table.symbolName);
|
|
39
|
+
if (prevSymbol) {
|
|
40
|
+
throw new Error(`Table symbol collision: tables "${prevSymbol}" and "${table.tableName}" both normalize to "${table.symbolName}"`);
|
|
41
|
+
}
|
|
42
|
+
symbols.set(table.symbolName, table.tableName);
|
|
43
|
+
const prevType = types.get(table.typeName);
|
|
44
|
+
if (prevType) {
|
|
45
|
+
throw new Error(`Type name collision: tables "${prevType}" and "${table.tableName}" both normalize to "${table.typeName}"`);
|
|
46
|
+
}
|
|
47
|
+
types.set(table.typeName, table.tableName);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const normalizeTables = (tables, options = {}) => {
|
|
51
|
+
const normalized = sortByName(tables)
|
|
52
|
+
.map((table) => {
|
|
53
|
+
const fields = sortByName(table.fields).map((field) => normalizeField(field, options));
|
|
54
|
+
ensureNoCollisions(table.name, fields);
|
|
55
|
+
const symbolName = normalizeTableSymbol(table.name);
|
|
56
|
+
const typeName = ensureIdentifier(pascalCase(table.singularName), 'Type');
|
|
57
|
+
return {
|
|
58
|
+
tableName: table.name,
|
|
59
|
+
singularName: table.singularName,
|
|
60
|
+
symbolName,
|
|
61
|
+
typeName,
|
|
62
|
+
referenceFieldName: table.referenceFieldName,
|
|
63
|
+
fields
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
ensureNoTableIdentifierCollisions(normalized);
|
|
67
|
+
return normalized;
|
|
68
|
+
};
|