@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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const planLiteral = (plan) => JSON.stringify(plan, null, 2);
|
|
2
|
+
export const buildCrudTestSource = (plan) => {
|
|
3
|
+
return `import { gridfoxTables } from '../generated/tables.js'
|
|
4
|
+
|
|
5
|
+
declare const process: {
|
|
6
|
+
env: Record<string, string | undefined>
|
|
7
|
+
exitCode?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const apiKey = process.env.GRIDFOX_API_KEY
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error('Missing GRIDFOX_API_KEY')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const apiBaseUrl = process.env.GRIDFOX_API_BASE_URL ?? 'https://api.gridfox.com'
|
|
16
|
+
|
|
17
|
+
const plan = ${planLiteral(plan)} as const
|
|
18
|
+
|
|
19
|
+
const request = async (method: string, path: string, body?: unknown): Promise<Response> => {
|
|
20
|
+
return fetch(new URL(path, apiBaseUrl), {
|
|
21
|
+
method,
|
|
22
|
+
headers: {
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
'gridfox-api-key': apiKey
|
|
25
|
+
},
|
|
26
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const assert = (condition: unknown, message: string): void => {
|
|
31
|
+
if (!condition) {
|
|
32
|
+
throw new Error(message)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizeKey = (value: string): string => value.toLowerCase().replace(/\\s+/g, '')
|
|
37
|
+
|
|
38
|
+
const asRecord = (value: unknown): Record<string, unknown> => {
|
|
39
|
+
if (!value || typeof value !== 'object') {
|
|
40
|
+
return {}
|
|
41
|
+
}
|
|
42
|
+
return value as Record<string, unknown>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getFieldValue = (record: Record<string, unknown>, fieldName: string): unknown => {
|
|
46
|
+
const target = normalizeKey(fieldName)
|
|
47
|
+
for (const [key, value] of Object.entries(record)) {
|
|
48
|
+
if (normalizeKey(key) === target) {
|
|
49
|
+
return value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const valuesEqual = (expected: unknown, actual: unknown): boolean => {
|
|
56
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
57
|
+
if (expected.length !== actual.length) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
const left = [...expected].map((entry) => JSON.stringify(entry)).sort()
|
|
61
|
+
const right = [...actual].map((entry) => JSON.stringify(entry)).sort()
|
|
62
|
+
return left.every((entry, index) => entry === right[index])
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(expected) === JSON.stringify(actual)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const replaceReferencePlaceholder = (payload: Record<string, unknown>, referenceValue: string): Record<string, unknown> => {
|
|
68
|
+
const out: Record<string, unknown> = {}
|
|
69
|
+
for (const [fieldName, value] of Object.entries(payload)) {
|
|
70
|
+
if (value === '__GRIDFOX_REFERENCE_VALUE__') {
|
|
71
|
+
out[fieldName] = referenceValue
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
out[fieldName] = value.map((entry) => (entry === '__GRIDFOX_REFERENCE_VALUE__' ? referenceValue : entry))
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
out[fieldName] = value
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const assertExpectedFields = (
|
|
84
|
+
record: Record<string, unknown>,
|
|
85
|
+
expectedValues: Record<string, unknown>,
|
|
86
|
+
label: string
|
|
87
|
+
): void => {
|
|
88
|
+
for (const [fieldName, expected] of Object.entries(expectedValues)) {
|
|
89
|
+
const actual = getFieldValue(record, fieldName)
|
|
90
|
+
assert(
|
|
91
|
+
valuesEqual(expected, actual),
|
|
92
|
+
label + ': expected field "' + fieldName + '" to equal ' + JSON.stringify(expected) + '; got ' + JSON.stringify(actual)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const main = async (): Promise<void> => {
|
|
98
|
+
const selectedTable = gridfoxTables.find((table) => table.tableName === plan.tableName)
|
|
99
|
+
if (!selectedTable) {
|
|
100
|
+
throw new Error('Selected table "' + plan.tableName + '" was not found in generated registry')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
assert(
|
|
104
|
+
selectedTable.referenceFieldName === plan.referenceFieldName,
|
|
105
|
+
'Reference field mismatch: plan=' + plan.referenceFieldName + ' generated=' + selectedTable.referenceFieldName
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const referenceValue = 'real-test-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10)
|
|
109
|
+
|
|
110
|
+
const createPayload = replaceReferencePlaceholder(plan.createPayload as Record<string, unknown>, referenceValue)
|
|
111
|
+
const updatePayload = replaceReferencePlaceholder(plan.updatePayload as Record<string, unknown>, referenceValue)
|
|
112
|
+
|
|
113
|
+
const createResponse = await request('POST', '/data/' + encodeURIComponent(selectedTable.tableName), createPayload)
|
|
114
|
+
assert(createResponse.status === 201, 'Create failed: expected 201, got ' + createResponse.status)
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const createBody = (await createResponse.json()) as { referenceFieldValue?: unknown }
|
|
118
|
+
assert(
|
|
119
|
+
createBody.referenceFieldValue === referenceValue,
|
|
120
|
+
'Create response referenceFieldValue mismatch: expected ' +
|
|
121
|
+
referenceValue +
|
|
122
|
+
', got ' +
|
|
123
|
+
JSON.stringify(createBody.referenceFieldValue)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const getAfterCreate = await request(
|
|
127
|
+
'GET',
|
|
128
|
+
'/data/' + encodeURIComponent(selectedTable.tableName) + '/' + encodeURIComponent(referenceValue)
|
|
129
|
+
)
|
|
130
|
+
assert(getAfterCreate.status === 200, 'Get-after-create failed: expected 200, got ' + getAfterCreate.status)
|
|
131
|
+
|
|
132
|
+
const createdRecord = asRecord(await getAfterCreate.json())
|
|
133
|
+
assertExpectedFields(
|
|
134
|
+
createdRecord,
|
|
135
|
+
replaceReferencePlaceholder(plan.verifyAfterCreate as Record<string, unknown>, referenceValue),
|
|
136
|
+
'after create'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const updateResponse = await request(
|
|
140
|
+
'PUT',
|
|
141
|
+
'/data/' + encodeURIComponent(selectedTable.tableName) + '/' + encodeURIComponent(referenceValue),
|
|
142
|
+
updatePayload
|
|
143
|
+
)
|
|
144
|
+
assert(updateResponse.status === 204, 'Update failed: expected 204, got ' + updateResponse.status)
|
|
145
|
+
|
|
146
|
+
const getAfterUpdate = await request(
|
|
147
|
+
'GET',
|
|
148
|
+
'/data/' + encodeURIComponent(selectedTable.tableName) + '/' + encodeURIComponent(referenceValue)
|
|
149
|
+
)
|
|
150
|
+
assert(getAfterUpdate.status === 200, 'Get-after-update failed: expected 200, got ' + getAfterUpdate.status)
|
|
151
|
+
|
|
152
|
+
const updatedRecord = asRecord(await getAfterUpdate.json())
|
|
153
|
+
assertExpectedFields(
|
|
154
|
+
updatedRecord,
|
|
155
|
+
replaceReferencePlaceholder(plan.verifyAfterUpdate as Record<string, unknown>, referenceValue),
|
|
156
|
+
'after update'
|
|
157
|
+
)
|
|
158
|
+
} finally {
|
|
159
|
+
const deleteResponse = await request(
|
|
160
|
+
'DELETE',
|
|
161
|
+
'/data/' + encodeURIComponent(selectedTable.tableName) + '/' + encodeURIComponent(referenceValue)
|
|
162
|
+
)
|
|
163
|
+
assert(deleteResponse.status === 204, 'Delete cleanup failed: expected 204, got ' + deleteResponse.status)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const getAfterDelete = await request(
|
|
167
|
+
'GET',
|
|
168
|
+
'/data/' + encodeURIComponent(selectedTable.tableName) + '/' + encodeURIComponent(referenceValue)
|
|
169
|
+
)
|
|
170
|
+
assert(getAfterDelete.status === 404, 'Get-after-delete failed: expected 404, got ' + getAfterDelete.status)
|
|
171
|
+
|
|
172
|
+
console.log('Real CRUD checks passed against table "' + plan.tableName + '"')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main().catch((error) => {
|
|
176
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
177
|
+
process.exitCode = 1
|
|
178
|
+
})
|
|
179
|
+
`;
|
|
180
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gridfox/codegen",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Schema-driven Gridfox TypeScript code generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"gridfox": "dist/cli/main.js",
|
|
18
|
+
"gridfox-codegen": "dist/cli/main.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
".env.example"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [],
|
|
32
|
+
"author": "",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"cac": "^7.0.0",
|
|
35
|
+
"change-case": "^5.4.4",
|
|
36
|
+
"prettier": "^3.8.1",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.3.5",
|
|
41
|
+
"execa": "^9.6.1",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "pnpm run build && vitest run",
|
|
48
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\"",
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"dev": "node dist/cli/main.js --help",
|
|
51
|
+
"test:real": "pnpm run build && node dist/cli/main.js validate",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"test:integration": "pnpm run build && vitest run tests/integration",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"generate": "pnpm run build && node dist/cli/main.js"
|
|
56
|
+
}
|
|
57
|
+
}
|