@skyf0xx/hedgehog 4.2.0 → 4.2.2
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 +20 -0
- package/bin/cli.mjs +12 -0
- package/package.json +2 -2
- package/src/agents/backend-eng.md +30 -16
- package/src/agents/front-end-eng.md +29 -12
- package/src/db/next.mjs +46 -8
- package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
- package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
- package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
- package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
- package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
- package/src/golden-cores/full-stack-app/nx.json +4 -1
- package/src/golden-cores/full-stack-app/package.json +8 -0
- package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
- package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
- package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
- package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
- package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
- package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +67 -6
- package/src/skills/hedgehog-loop/SKILL.md +119 -53
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerates apps/api/src/app/feature-modules.ts — the barrel that lists
|
|
4
|
+
* every domain module's NestJS `@Module` class for AppModule to import.
|
|
5
|
+
*
|
|
6
|
+
* Written as CommonJS (`.cjs`) deliberately: root `package.json` sets
|
|
7
|
+
* `"type": "module"`, so a plain `.js` file here would be parsed as ESM
|
|
8
|
+
* and `require`/`module.exports` below would fail with "require is not
|
|
9
|
+
* defined".
|
|
10
|
+
*
|
|
11
|
+
* Why generated rather than hand-edited: the controller layer's scope is
|
|
12
|
+
* `apps/api/src/app/{module}/**` (core.yaml) — module-disjoint by
|
|
13
|
+
* construction, so two modules' controller tasks never touch the same
|
|
14
|
+
* file. AppModule itself is exclusive: true / once (see core.yaml), so it
|
|
15
|
+
* can't carry a per-module edit either. A file that every module's
|
|
16
|
+
* controller task hand-edited (even one line each) would sit outside
|
|
17
|
+
* every task's declared scope, invisible to the scheduler's conflict
|
|
18
|
+
* check (src/db/conflict.mjs only compares declared scope globs) — two
|
|
19
|
+
* concurrent module builds editing it would look conflict-free to the
|
|
20
|
+
* scheduler while actually racing on one file. Generating it instead
|
|
21
|
+
* removes the shared edit entirely: every module writes only inside its
|
|
22
|
+
* own `apps/api/src/app/{module}/` directory (a `{module}.module.ts`
|
|
23
|
+
* file, discovered here by naming convention), and this script is the
|
|
24
|
+
* only thing that ever writes feature-modules.ts.
|
|
25
|
+
*
|
|
26
|
+
* Convention: any file matching apps/api/src/app/*\/*.module.ts (module
|
|
27
|
+
* directories only, one level deep — not app.module.ts itself) is treated
|
|
28
|
+
* as a domain module and must have a named export ending in "Module".
|
|
29
|
+
*
|
|
30
|
+
* Run via the `generate-feature-modules` Nx target, which `test` and
|
|
31
|
+
* `build` depend on (nx.json targetDefaults) — never run by hand as part
|
|
32
|
+
* of normal development.
|
|
33
|
+
*/
|
|
34
|
+
const fs = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
|
|
37
|
+
const APP_DIR = path.join(__dirname, '..', 'apps', 'api', 'src', 'app');
|
|
38
|
+
const OUTPUT_FILE = path.join(APP_DIR, 'feature-modules.ts');
|
|
39
|
+
const HEADER =
|
|
40
|
+
'// GENERATED FILE — do not hand-edit. Regenerated by\n' +
|
|
41
|
+
'// tools/generate-feature-modules.cjs (the generate-feature-modules Nx\n' +
|
|
42
|
+
'// target, which test and build depend on) from every\n' +
|
|
43
|
+
"// apps/api/src/app/*/*.module.ts file on disk. A module directory's\n" +
|
|
44
|
+
'// controller layer only ever creates its own *.module.ts inside its own\n' +
|
|
45
|
+
'// directory — never edits this file.\n';
|
|
46
|
+
|
|
47
|
+
function findModuleFiles() {
|
|
48
|
+
if (!fs.existsSync(APP_DIR)) return [];
|
|
49
|
+
const entries = fs.readdirSync(APP_DIR, { withFileTypes: true });
|
|
50
|
+
const files = [];
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
const moduleDir = path.join(APP_DIR, entry.name);
|
|
54
|
+
for (const file of fs.readdirSync(moduleDir)) {
|
|
55
|
+
if (file.endsWith('.module.ts')) {
|
|
56
|
+
files.push(path.join(entry.name, file.replace(/\.ts$/, '')));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return files.sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function exportNameFor(relativePath) {
|
|
64
|
+
// "tasks/tasks.module" -> "TasksModule". Every domain module generator
|
|
65
|
+
// (Nx's @nx/nest:module or hand-authored) names its class
|
|
66
|
+
// `<PascalCase>Module` to match the file's own `<kebab>.module.ts` name
|
|
67
|
+
// — the same convention @nx/nest itself uses.
|
|
68
|
+
const base = path.basename(relativePath).replace(/\.module$/, '');
|
|
69
|
+
const pascal = base
|
|
70
|
+
.split('-')
|
|
71
|
+
.map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1))
|
|
72
|
+
.join('');
|
|
73
|
+
return `${pascal}Module`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function main() {
|
|
77
|
+
const moduleFiles = findModuleFiles();
|
|
78
|
+
const imports = [];
|
|
79
|
+
const names = [];
|
|
80
|
+
|
|
81
|
+
for (const relativePath of moduleFiles) {
|
|
82
|
+
const importName = exportNameFor(relativePath);
|
|
83
|
+
imports.push(`import { ${importName} } from './${relativePath}';`);
|
|
84
|
+
names.push(importName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const body =
|
|
88
|
+
names.length > 0
|
|
89
|
+
? `export const featureModules = [${names.join(', ')}];\n`
|
|
90
|
+
: 'export const featureModules = [];\n';
|
|
91
|
+
|
|
92
|
+
const content =
|
|
93
|
+
HEADER +
|
|
94
|
+
(imports.length > 0 ? '\n' + imports.join('\n') + '\n' : '') +
|
|
95
|
+
'\n' +
|
|
96
|
+
body;
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(OUTPUT_FILE, content);
|
|
99
|
+
console.log(
|
|
100
|
+
`Generated ${path.relative(process.cwd(), OUTPUT_FILE)} with ${names.length} feature module(s).`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main();
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { formatFiles, Tree } from '@nx/devkit';
|
|
2
|
+
import { Field, isDateField, parseFields, zodSchema } from '../fields';
|
|
3
|
+
import { appendBarrelExport, generateLibShell } from '../lib-shell';
|
|
4
|
+
import { moduleNames, ModuleNames } from '../naming';
|
|
5
|
+
|
|
6
|
+
interface ContractGeneratorOptions {
|
|
7
|
+
module: string;
|
|
8
|
+
fields: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PACKAGE_ROOT = 'packages/contracts';
|
|
12
|
+
|
|
13
|
+
export default async function contractGenerator(
|
|
14
|
+
tree: Tree,
|
|
15
|
+
options: ContractGeneratorOptions,
|
|
16
|
+
) {
|
|
17
|
+
const names = moduleNames(options.module);
|
|
18
|
+
const fields = parseFields(options.fields);
|
|
19
|
+
|
|
20
|
+
await generateLibShell(tree, {
|
|
21
|
+
directory: PACKAGE_ROOT,
|
|
22
|
+
importName: 'contracts',
|
|
23
|
+
tags: ['scope:contracts', 'type:contract'],
|
|
24
|
+
dependencies: {
|
|
25
|
+
'@ts-rest/core': '3.53.0-rc.1',
|
|
26
|
+
zod: '^4.4.3',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
writeTimestampSchema(tree);
|
|
31
|
+
|
|
32
|
+
const dir = `${PACKAGE_ROOT}/src/${names.module}`;
|
|
33
|
+
tree.write(`${dir}/${names.module}.schema.ts`, entitySchemaFile(names, fields));
|
|
34
|
+
tree.write(`${dir}/${names.module}.errors.ts`, errorSchemaFile(names));
|
|
35
|
+
tree.write(`${dir}/${names.module}.contract.ts`, contractFile(names));
|
|
36
|
+
tree.write(`${dir}/index.ts`, barrelFile(names));
|
|
37
|
+
tree.write(`${dir}/${names.module}.spec.ts`, specFile(names, fields));
|
|
38
|
+
|
|
39
|
+
appendBarrelExport(tree, `${PACKAGE_ROOT}/src/index.ts`, `./${names.module}/index`);
|
|
40
|
+
|
|
41
|
+
await formatFiles(tree);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The shared half of the date-mode rule: a `timestamp` column declared
|
|
46
|
+
* `mode: 'date'` reflects through `createSelectSchema` as `z.date()`, which
|
|
47
|
+
* the server satisfies with a real `Date` and the browser never can — JSON
|
|
48
|
+
* carries it as an ISO string. Neither side alone is right, and no
|
|
49
|
+
* `.transform()` satisfies both, so the contract accepts the union.
|
|
50
|
+
*/
|
|
51
|
+
function writeTimestampSchema(tree: Tree) {
|
|
52
|
+
const path = `${PACKAGE_ROOT}/src/timestamp.ts`;
|
|
53
|
+
if (tree.exists(path)) return;
|
|
54
|
+
|
|
55
|
+
tree.write(
|
|
56
|
+
path,
|
|
57
|
+
`import { z } from 'zod';
|
|
58
|
+
|
|
59
|
+
export const timestampSchema = z.union([z.date(), z.iso.datetime()]);
|
|
60
|
+
|
|
61
|
+
export type Timestamp = z.infer<typeof timestampSchema>;
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
64
|
+
appendBarrelExport(tree, `${PACKAGE_ROOT}/src/index.ts`, './timestamp');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function entitySchemaFile(names: ModuleNames, fields: Field[]): string {
|
|
68
|
+
const bodyFields = fields
|
|
69
|
+
.map((field) => ` ${field.name}: ${zodSchema(field)},`)
|
|
70
|
+
.join('\n');
|
|
71
|
+
const createFields = fields
|
|
72
|
+
.map(
|
|
73
|
+
(field) =>
|
|
74
|
+
` ${field.name}: ${zodSchema(field)}${field.nullable ? '.optional()' : ''},`,
|
|
75
|
+
)
|
|
76
|
+
.join('\n');
|
|
77
|
+
|
|
78
|
+
return `import { z } from 'zod';
|
|
79
|
+
import { timestampSchema } from '../timestamp';
|
|
80
|
+
|
|
81
|
+
export const ${names.entityCamel}Schema = z.object({
|
|
82
|
+
id: z.uuid(),
|
|
83
|
+
${bodyFields}
|
|
84
|
+
createdAt: timestampSchema,
|
|
85
|
+
updatedAt: timestampSchema,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const create${names.entityPascal}Schema = z.object({
|
|
89
|
+
${createFields}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const update${names.entityPascal}Schema = create${names.entityPascal}Schema.partial();
|
|
93
|
+
|
|
94
|
+
export type ${names.entityPascal} = z.infer<typeof ${names.entityCamel}Schema>;
|
|
95
|
+
export type Create${names.entityPascal} = z.infer<typeof create${names.entityPascal}Schema>;
|
|
96
|
+
export type Update${names.entityPascal} = z.infer<typeof update${names.entityPascal}Schema>;
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function errorSchemaFile(names: ModuleNames): string {
|
|
101
|
+
return `import { z } from 'zod';
|
|
102
|
+
|
|
103
|
+
export const ${names.entityCamel}NotFoundSchema = z.object({
|
|
104
|
+
error: z.literal('${names.entityPascal}NotFound'),
|
|
105
|
+
message: z.string(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const ${names.entityCamel}BadRequestSchema = z.object({
|
|
109
|
+
error: z.literal('${names.entityPascal}BadRequest'),
|
|
110
|
+
message: z.string(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export type ${names.entityPascal}NotFound = z.infer<typeof ${names.entityCamel}NotFoundSchema>;
|
|
114
|
+
export type ${names.entityPascal}BadRequest = z.infer<typeof ${names.entityCamel}BadRequestSchema>;
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function contractFile(names: ModuleNames): string {
|
|
119
|
+
return `import { initContract } from '@ts-rest/core';
|
|
120
|
+
import { z } from 'zod';
|
|
121
|
+
import {
|
|
122
|
+
${names.entityCamel}Schema,
|
|
123
|
+
create${names.entityPascal}Schema,
|
|
124
|
+
update${names.entityPascal}Schema,
|
|
125
|
+
} from './${names.module}.schema';
|
|
126
|
+
import {
|
|
127
|
+
${names.entityCamel}BadRequestSchema,
|
|
128
|
+
${names.entityCamel}NotFoundSchema,
|
|
129
|
+
} from './${names.module}.errors';
|
|
130
|
+
|
|
131
|
+
const c = initContract();
|
|
132
|
+
|
|
133
|
+
export const ${names.camel}Contract = c.router(
|
|
134
|
+
{
|
|
135
|
+
list: {
|
|
136
|
+
method: 'GET',
|
|
137
|
+
path: '/${names.module}',
|
|
138
|
+
responses: {
|
|
139
|
+
200: z.array(${names.entityCamel}Schema),
|
|
140
|
+
},
|
|
141
|
+
summary: 'List every ${names.entityCamel}',
|
|
142
|
+
},
|
|
143
|
+
get: {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
path: '/${names.module}/:id',
|
|
146
|
+
pathParams: z.object({ id: z.uuid() }),
|
|
147
|
+
responses: {
|
|
148
|
+
200: ${names.entityCamel}Schema,
|
|
149
|
+
404: ${names.entityCamel}NotFoundSchema,
|
|
150
|
+
},
|
|
151
|
+
summary: 'Fetch one ${names.entityCamel} by id',
|
|
152
|
+
},
|
|
153
|
+
create: {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
path: '/${names.module}',
|
|
156
|
+
body: create${names.entityPascal}Schema,
|
|
157
|
+
responses: {
|
|
158
|
+
201: ${names.entityCamel}Schema,
|
|
159
|
+
400: ${names.entityCamel}BadRequestSchema,
|
|
160
|
+
},
|
|
161
|
+
summary: 'Create a ${names.entityCamel}',
|
|
162
|
+
},
|
|
163
|
+
update: {
|
|
164
|
+
method: 'PATCH',
|
|
165
|
+
path: '/${names.module}/:id',
|
|
166
|
+
pathParams: z.object({ id: z.uuid() }),
|
|
167
|
+
body: update${names.entityPascal}Schema,
|
|
168
|
+
responses: {
|
|
169
|
+
200: ${names.entityCamel}Schema,
|
|
170
|
+
400: ${names.entityCamel}BadRequestSchema,
|
|
171
|
+
404: ${names.entityCamel}NotFoundSchema,
|
|
172
|
+
},
|
|
173
|
+
summary: 'Update a ${names.entityCamel}',
|
|
174
|
+
},
|
|
175
|
+
remove: {
|
|
176
|
+
method: 'DELETE',
|
|
177
|
+
path: '/${names.module}/:id',
|
|
178
|
+
pathParams: z.object({ id: z.uuid() }),
|
|
179
|
+
responses: {
|
|
180
|
+
204: c.noBody(),
|
|
181
|
+
404: ${names.entityCamel}NotFoundSchema,
|
|
182
|
+
},
|
|
183
|
+
summary: 'Delete a ${names.entityCamel}',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
// apps/api sets /api as a global prefix at runtime (apps/api/src/main.ts),
|
|
188
|
+
// so the paths above stay prefix-free and the client adds the base URL.
|
|
189
|
+
strictStatusCodes: true,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function barrelFile(names: ModuleNames): string {
|
|
196
|
+
return `export * from './${names.module}.contract';
|
|
197
|
+
export * from './${names.module}.errors';
|
|
198
|
+
export * from './${names.module}.schema';
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function specFile(names: ModuleNames, fields: Field[]): string {
|
|
203
|
+
const dateField = fields.find(isDateField);
|
|
204
|
+
const requiredField = fields.find((field) => !field.nullable);
|
|
205
|
+
|
|
206
|
+
return `import { describe, expect, it } from 'vitest';
|
|
207
|
+
import {
|
|
208
|
+
${names.camel}Contract,
|
|
209
|
+
${names.entityCamel}Schema,
|
|
210
|
+
create${names.entityPascal}Schema,
|
|
211
|
+
} from './index';
|
|
212
|
+
|
|
213
|
+
const sample = ${sampleLiteral(names, fields)};
|
|
214
|
+
|
|
215
|
+
describe('${names.camel} contract', () => {
|
|
216
|
+
it('exposes the full CRUD route set under /${names.module}', () => {
|
|
217
|
+
expect(${names.camel}Contract.list.method).toBe('GET');
|
|
218
|
+
expect(${names.camel}Contract.list.path).toBe('/${names.module}');
|
|
219
|
+
expect(${names.camel}Contract.get.path).toBe('/${names.module}/:id');
|
|
220
|
+
expect(${names.camel}Contract.create.method).toBe('POST');
|
|
221
|
+
expect(${names.camel}Contract.update.method).toBe('PATCH');
|
|
222
|
+
expect(${names.camel}Contract.remove.method).toBe('DELETE');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('accepts a server-side row, where every timestamp is a Date', () => {
|
|
226
|
+
expect(${names.entityCamel}Schema.parse(sample)).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('accepts the same row as JSON, where every timestamp is an ISO string', () => {
|
|
230
|
+
const overTheWire = {
|
|
231
|
+
...sample,${dateField ? `\n ${dateField.name}: sample.${dateField.name}.toISOString(),` : ''}
|
|
232
|
+
createdAt: sample.createdAt.toISOString(),
|
|
233
|
+
updatedAt: sample.updatedAt.toISOString(),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
expect(${names.entityCamel}Schema.parse(overTheWire)).toBeDefined();
|
|
237
|
+
});
|
|
238
|
+
${
|
|
239
|
+
requiredField
|
|
240
|
+
? `
|
|
241
|
+
it('rejects a create body missing ${requiredField.name}', () => {
|
|
242
|
+
const rest = { ...createInput } as Partial<typeof createInput>;
|
|
243
|
+
delete rest.${requiredField.name};
|
|
244
|
+
|
|
245
|
+
expect(create${names.entityPascal}Schema.safeParse(rest).success).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
`
|
|
248
|
+
: ''
|
|
249
|
+
}});
|
|
250
|
+
|
|
251
|
+
const createInput = ${createLiteral(fields)};
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const SAMPLE_UUID = '00000000-0000-4000-8000-000000000000';
|
|
256
|
+
|
|
257
|
+
function sampleLiteral(names: ModuleNames, fields: Field[]): string {
|
|
258
|
+
const entries = [
|
|
259
|
+
`id: '${SAMPLE_UUID}'`,
|
|
260
|
+
...fields.map((field) => `${field.name}: ${sampleValue(field)}`),
|
|
261
|
+
'createdAt: new Date()',
|
|
262
|
+
'updatedAt: new Date()',
|
|
263
|
+
];
|
|
264
|
+
return `{\n ${entries.join(',\n ')},\n}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createLiteral(fields: Field[]): string {
|
|
268
|
+
const entries = fields
|
|
269
|
+
.filter((field) => !field.nullable)
|
|
270
|
+
.map((field) => `${field.name}: ${sampleValue(field)}`);
|
|
271
|
+
return `{\n ${entries.join(',\n ')},\n}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sampleValue(field: Field): string {
|
|
275
|
+
const byType: Record<string, string> = {
|
|
276
|
+
string: `'sample'`,
|
|
277
|
+
text: `'sample'`,
|
|
278
|
+
boolean: 'false',
|
|
279
|
+
integer: '1',
|
|
280
|
+
timestamp: 'new Date()',
|
|
281
|
+
};
|
|
282
|
+
return byType[field.type] ?? 'null';
|
|
283
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "HedgehogContractLayer",
|
|
4
|
+
"title": "ts-rest contract for one domain module",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"module": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Domain module name, plural kebab-case (e.g. tasks, order-items).",
|
|
10
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
11
|
+
"x-prompt": "Domain module name (plural kebab-case)?"
|
|
12
|
+
},
|
|
13
|
+
"fields": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The same name:type list the schema layer was generated with; a trailing ? marks the field nullable.",
|
|
16
|
+
"x-prompt": "Fields (name:type, comma-separated)?"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": ["module", "fields"]
|
|
20
|
+
}
|