archetype-engine 2.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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/src/ai/adapters/anthropic.d.ts +31 -0
- package/dist/src/ai/adapters/anthropic.d.ts.map +1 -0
- package/dist/src/ai/adapters/anthropic.js +75 -0
- package/dist/src/ai/adapters/openai.d.ts +33 -0
- package/dist/src/ai/adapters/openai.d.ts.map +1 -0
- package/dist/src/ai/adapters/openai.js +120 -0
- package/dist/src/ai/adapters/vercel.d.ts +434 -0
- package/dist/src/ai/adapters/vercel.d.ts.map +1 -0
- package/dist/src/ai/adapters/vercel.js +162 -0
- package/dist/src/ai/index.d.ts +492 -0
- package/dist/src/ai/index.d.ts.map +1 -0
- package/dist/src/ai/index.js +71 -0
- package/dist/src/ai/state.d.ts +13 -0
- package/dist/src/ai/state.d.ts.map +1 -0
- package/dist/src/ai/state.js +215 -0
- package/dist/src/ai/tools.d.ts +13 -0
- package/dist/src/ai/tools.d.ts.map +1 -0
- package/dist/src/ai/tools.js +257 -0
- package/dist/src/ai/types.d.ts +196 -0
- package/dist/src/ai/types.d.ts.map +1 -0
- package/dist/src/ai/types.js +9 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +540 -0
- package/dist/src/core/utils.d.ts +27 -0
- package/dist/src/core/utils.d.ts.map +1 -0
- package/dist/src/core/utils.js +56 -0
- package/dist/src/entity.d.ts +165 -0
- package/dist/src/entity.d.ts.map +1 -0
- package/dist/src/entity.js +108 -0
- package/dist/src/fields.d.ts +207 -0
- package/dist/src/fields.d.ts.map +1 -0
- package/dist/src/fields.js +291 -0
- package/dist/src/generators/erd-ir.d.ts +10 -0
- package/dist/src/generators/erd-ir.d.ts.map +1 -0
- package/dist/src/generators/erd-ir.js +119 -0
- package/dist/src/index.d.ts +51 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +101 -0
- package/dist/src/init/dependencies.d.ts +31 -0
- package/dist/src/init/dependencies.d.ts.map +1 -0
- package/dist/src/init/dependencies.js +101 -0
- package/dist/src/init/entity-templates.d.ts +42 -0
- package/dist/src/init/entity-templates.d.ts.map +1 -0
- package/dist/src/init/entity-templates.js +367 -0
- package/dist/src/init/index.d.ts +10 -0
- package/dist/src/init/index.d.ts.map +1 -0
- package/dist/src/init/index.js +250 -0
- package/dist/src/init/prompts.d.ts +11 -0
- package/dist/src/init/prompts.d.ts.map +1 -0
- package/dist/src/init/prompts.js +275 -0
- package/dist/src/init/templates.d.ts +24 -0
- package/dist/src/init/templates.d.ts.map +1 -0
- package/dist/src/init/templates.js +587 -0
- package/dist/src/json/index.d.ts +11 -0
- package/dist/src/json/index.d.ts.map +1 -0
- package/dist/src/json/index.js +26 -0
- package/dist/src/json/parser.d.ts +61 -0
- package/dist/src/json/parser.d.ts.map +1 -0
- package/dist/src/json/parser.js +309 -0
- package/dist/src/json/types.d.ts +275 -0
- package/dist/src/json/types.d.ts.map +1 -0
- package/dist/src/json/types.js +10 -0
- package/dist/src/manifest.d.ts +147 -0
- package/dist/src/manifest.d.ts.map +1 -0
- package/dist/src/manifest.js +104 -0
- package/dist/src/relations.d.ts +96 -0
- package/dist/src/relations.d.ts.map +1 -0
- package/dist/src/relations.js +108 -0
- package/dist/src/source.d.ts +93 -0
- package/dist/src/source.d.ts.map +1 -0
- package/dist/src/source.js +89 -0
- package/dist/src/template/context.d.ts +34 -0
- package/dist/src/template/context.d.ts.map +1 -0
- package/dist/src/template/context.js +31 -0
- package/dist/src/template/index.d.ts +6 -0
- package/dist/src/template/index.d.ts.map +1 -0
- package/dist/src/template/index.js +12 -0
- package/dist/src/template/registry.d.ts +18 -0
- package/dist/src/template/registry.d.ts.map +1 -0
- package/dist/src/template/registry.js +89 -0
- package/dist/src/template/runner.d.ts +9 -0
- package/dist/src/template/runner.d.ts.map +1 -0
- package/dist/src/template/runner.js +125 -0
- package/dist/src/template/types.d.ts +73 -0
- package/dist/src/template/types.d.ts.map +1 -0
- package/dist/src/template/types.js +3 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/api.d.ts +22 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/api.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/api.js +866 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/auth.d.ts +20 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/auth.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/auth.js +273 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/crud-hooks.d.ts +22 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/crud-hooks.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/crud-hooks.js +237 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/hooks.d.ts +30 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/hooks.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/hooks.js +345 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/i18n.d.ts +25 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/i18n.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/i18n.js +199 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.d.ts +8 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.js +18 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/schema.d.ts +22 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/schema.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/schema.js +270 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/service.d.ts +23 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/service.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/service.js +304 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/validation.d.ts +21 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/validation.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/validation.js +248 -0
- package/dist/src/templates/nextjs-drizzle-trpc/index.d.ts +30 -0
- package/dist/src/templates/nextjs-drizzle-trpc/index.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/index.js +71 -0
- package/dist/src/validation/index.d.ts +71 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +314 -0
- package/package.json +86 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Drizzle ORM Schema Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates database table definitions for Drizzle ORM from entity definitions.
|
|
6
|
+
* Supports SQLite, PostgreSQL, and MySQL databases.
|
|
7
|
+
*
|
|
8
|
+
* Generated files:
|
|
9
|
+
* - db/schema.ts - All entity table definitions and junction tables
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Maps field types to appropriate database column types
|
|
13
|
+
* - Handles required/optional fields with notNull()
|
|
14
|
+
* - Creates foreign key references for hasOne relations
|
|
15
|
+
* - Generates junction tables for belongsToMany relations
|
|
16
|
+
* - Adds timestamp fields (createdAt, updatedAt) when behaviors.timestamps is true
|
|
17
|
+
* - Adds deletedAt field when behaviors.softDelete is true
|
|
18
|
+
*
|
|
19
|
+
* @module generators/schema
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.schemaGenerator = void 0;
|
|
23
|
+
/**
|
|
24
|
+
* Map entity field type to Drizzle column type
|
|
25
|
+
*
|
|
26
|
+
* @param config - Field configuration from entity definition
|
|
27
|
+
* @param isSqlite - Whether target database is SQLite
|
|
28
|
+
* @returns Drizzle column type name
|
|
29
|
+
*/
|
|
30
|
+
function mapFieldType(config, isSqlite) {
|
|
31
|
+
switch (config.type) {
|
|
32
|
+
case 'text': return 'text';
|
|
33
|
+
case 'number': return 'integer';
|
|
34
|
+
case 'boolean': return isSqlite ? 'integer' : 'boolean';
|
|
35
|
+
case 'date': return 'text';
|
|
36
|
+
case 'enum': return 'text'; // Enum uses text for SQLite, pgEnum for PG (handled separately)
|
|
37
|
+
default: return 'text';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate enum type name from field name
|
|
42
|
+
*/
|
|
43
|
+
function getEnumTypeName(entityName, fieldName) {
|
|
44
|
+
return `${entityName.toLowerCase()}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Enum`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Collect all enum fields from entities and generate pgEnum definitions
|
|
48
|
+
*/
|
|
49
|
+
function generateEnumDefinitions(manifest) {
|
|
50
|
+
const enums = [];
|
|
51
|
+
const generated = new Set();
|
|
52
|
+
for (const entity of manifest.entities) {
|
|
53
|
+
for (const [fieldName, config] of Object.entries(entity.fields)) {
|
|
54
|
+
if (config.type === 'enum' && config.enumValues) {
|
|
55
|
+
const enumName = config.enumName || getEnumTypeName(entity.name, fieldName);
|
|
56
|
+
if (!generated.has(enumName)) {
|
|
57
|
+
generated.add(enumName);
|
|
58
|
+
const values = config.enumValues.map(v => `'${v}'`).join(', ');
|
|
59
|
+
enums.push(`export const ${enumName} = pgEnum('${enumName}', [${values}])`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return enums.join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate a single field definition line for the schema
|
|
68
|
+
*
|
|
69
|
+
* @param fieldName - Name of the field in the entity
|
|
70
|
+
* @param config - Field configuration with type, validations, etc.
|
|
71
|
+
* @param isSqlite - Whether target database is SQLite
|
|
72
|
+
* @param ctx - Generator context with naming utilities
|
|
73
|
+
* @param entityName - Entity name for enum type naming
|
|
74
|
+
* @returns Drizzle field definition code (e.g., "email: text('email').notNull().unique(),")
|
|
75
|
+
*/
|
|
76
|
+
function generateFieldDefinition(fieldName, config, isSqlite, ctx, entityName) {
|
|
77
|
+
const columnName = ctx.naming.getColumnName(fieldName);
|
|
78
|
+
const parts = [];
|
|
79
|
+
// Handle enum fields specially
|
|
80
|
+
if (config.type === 'enum' && config.enumValues) {
|
|
81
|
+
if (isSqlite) {
|
|
82
|
+
// SQLite: use text column (validation enforced by Zod)
|
|
83
|
+
parts.push(`text('${columnName}')`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// PostgreSQL: use pgEnum reference
|
|
87
|
+
const enumName = config.enumName || (entityName ? getEnumTypeName(entityName, fieldName) : `${fieldName}Enum`);
|
|
88
|
+
parts.push(`${enumName}('${columnName}')`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (config.type === 'boolean' && isSqlite) {
|
|
92
|
+
parts.push(`integer('${columnName}', { mode: 'boolean' })`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const type = mapFieldType(config, isSqlite);
|
|
96
|
+
parts.push(`${type}('${columnName}')`);
|
|
97
|
+
}
|
|
98
|
+
if (config.required)
|
|
99
|
+
parts.push('.notNull()');
|
|
100
|
+
if (config.unique)
|
|
101
|
+
parts.push('.unique()');
|
|
102
|
+
if (config.default !== undefined) {
|
|
103
|
+
if (typeof config.default === 'string') {
|
|
104
|
+
parts.push(`.default('${config.default}')`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
parts.push(`.default(${config.default})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ` ${fieldName}: ${parts.join('')},`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate complete table definition for an entity
|
|
114
|
+
*
|
|
115
|
+
* @param entity - Compiled entity IR with fields, relations, and behaviors
|
|
116
|
+
* @param isSqlite - Whether target database is SQLite
|
|
117
|
+
* @param ctx - Generator context with naming utilities
|
|
118
|
+
* @returns Complete Drizzle table definition code
|
|
119
|
+
*/
|
|
120
|
+
function generateEntity(entity, isSqlite, ctx) {
|
|
121
|
+
const tableName = ctx.naming.getTableName(entity.name);
|
|
122
|
+
const tableFunc = isSqlite ? 'sqliteTable' : 'pgTable';
|
|
123
|
+
const lines = [];
|
|
124
|
+
lines.push(`export const ${tableName} = ${tableFunc}('${tableName}', {`);
|
|
125
|
+
lines.push(` id: text('id').primaryKey(),`);
|
|
126
|
+
// User-defined fields (skip computed fields - they're not stored in DB)
|
|
127
|
+
for (const [fieldName, config] of Object.entries(entity.fields)) {
|
|
128
|
+
if (config.type === 'computed')
|
|
129
|
+
continue;
|
|
130
|
+
lines.push(generateFieldDefinition(fieldName, config, isSqlite, ctx, entity.name));
|
|
131
|
+
}
|
|
132
|
+
// Foreign keys for hasOne relations (skip if field already exists)
|
|
133
|
+
for (const [relName, rel] of Object.entries(entity.relations)) {
|
|
134
|
+
if (rel.type === 'hasOne') {
|
|
135
|
+
const fkField = rel.field || `${relName}Id`;
|
|
136
|
+
// Skip if this field was already defined in entity.fields
|
|
137
|
+
if (entity.fields[fkField])
|
|
138
|
+
continue;
|
|
139
|
+
// Skip self-referential relations - they cause TypeScript issues with inline references
|
|
140
|
+
if (rel.entity === entity.name) {
|
|
141
|
+
const fkColumn = ctx.naming.getColumnName(fkField);
|
|
142
|
+
lines.push(` ${fkField}: text('${fkColumn}'), // Self-reference handled via relations`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const fkColumn = ctx.naming.getColumnName(fkField);
|
|
146
|
+
const targetTable = ctx.naming.getTableName(rel.entity);
|
|
147
|
+
const notNull = rel.optional ? '' : '.notNull()';
|
|
148
|
+
lines.push(` ${fkField}: text('${fkColumn}').references(() => ${targetTable}.id)${notNull},`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Timestamps
|
|
152
|
+
if (entity.behaviors.timestamps) {
|
|
153
|
+
lines.push(` createdAt: text('created_at').notNull(),`);
|
|
154
|
+
lines.push(` updatedAt: text('updated_at').notNull(),`);
|
|
155
|
+
}
|
|
156
|
+
// Soft delete
|
|
157
|
+
if (entity.behaviors.softDelete) {
|
|
158
|
+
lines.push(` deletedAt: text('deleted_at'),`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('})');
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Generate pivot fields for junction tables
|
|
165
|
+
*
|
|
166
|
+
* @param fields - Pivot field configurations
|
|
167
|
+
* @param isSqlite - Whether target database is SQLite
|
|
168
|
+
* @param ctx - Generator context
|
|
169
|
+
* @returns Array of field definition lines
|
|
170
|
+
*/
|
|
171
|
+
function generatePivotFields(fields, isSqlite, ctx) {
|
|
172
|
+
if (!fields)
|
|
173
|
+
return [];
|
|
174
|
+
return Object.entries(fields).map(([fieldName, config]) => generateFieldDefinition(fieldName, config, isSqlite, ctx));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generate junction tables for many-to-many relations
|
|
178
|
+
*
|
|
179
|
+
* Creates tables for belongsToMany relations. Handles both regular many-to-many
|
|
180
|
+
* and self-referential relations (e.g., User following User).
|
|
181
|
+
* Supports pivot fields for additional data on the relation.
|
|
182
|
+
*
|
|
183
|
+
* @param manifest - Compiled manifest with all entities
|
|
184
|
+
* @param isSqlite - Whether target database is SQLite
|
|
185
|
+
* @param ctx - Generator context with naming utilities
|
|
186
|
+
* @returns Junction table definitions code
|
|
187
|
+
*/
|
|
188
|
+
function generateJunctionTables(manifest, isSqlite, ctx) {
|
|
189
|
+
const tableFunc = isSqlite ? 'sqliteTable' : 'pgTable';
|
|
190
|
+
const tables = [];
|
|
191
|
+
const generated = new Set();
|
|
192
|
+
for (const entity of manifest.entities) {
|
|
193
|
+
for (const [relName, rel] of Object.entries(entity.relations)) {
|
|
194
|
+
if (rel.type === 'belongsToMany') {
|
|
195
|
+
const table1 = ctx.naming.getTableName(entity.name);
|
|
196
|
+
const table2 = ctx.naming.getTableName(rel.entity);
|
|
197
|
+
// Generate pivot fields if defined
|
|
198
|
+
const pivotFields = generatePivotFields(rel.pivot?.fields, isSqlite, ctx);
|
|
199
|
+
const pivotFieldsStr = pivotFields.length > 0 ? '\n' + pivotFields.join('\n') : '';
|
|
200
|
+
if (entity.name === rel.entity) {
|
|
201
|
+
// Self-referential
|
|
202
|
+
const junctionName = rel.pivot?.table || `${entity.name.toLowerCase()}_${ctx.naming.toSnakeCase(relName)}`;
|
|
203
|
+
if (!generated.has(junctionName)) {
|
|
204
|
+
generated.add(junctionName);
|
|
205
|
+
tables.push(`
|
|
206
|
+
export const ${ctx.naming.toCamelCase(junctionName)} = ${tableFunc}('${junctionName}', {
|
|
207
|
+
sourceId: text('source_id').notNull().references(() => ${table1}.id),
|
|
208
|
+
targetId: text('target_id').notNull().references(() => ${table2}.id),${pivotFieldsStr}
|
|
209
|
+
})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (entity.name < rel.entity) {
|
|
213
|
+
// Custom table name from pivot config or default
|
|
214
|
+
const junctionName = rel.pivot?.table || `${entity.name.toLowerCase()}_${rel.entity.toLowerCase()}`;
|
|
215
|
+
if (!generated.has(junctionName)) {
|
|
216
|
+
generated.add(junctionName);
|
|
217
|
+
tables.push(`
|
|
218
|
+
export const ${ctx.naming.toCamelCase(junctionName)} = ${tableFunc}('${junctionName}', {
|
|
219
|
+
${entity.name.toLowerCase()}Id: text('${ctx.naming.toSnakeCase(entity.name)}_id').notNull().references(() => ${table1}.id),
|
|
220
|
+
${rel.entity.toLowerCase()}Id: text('${ctx.naming.toSnakeCase(rel.entity)}_id').notNull().references(() => ${table2}.id),${pivotFieldsStr}
|
|
221
|
+
})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return tables.join('\n');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if manifest has any enum fields
|
|
231
|
+
*/
|
|
232
|
+
function hasEnumFields(manifest) {
|
|
233
|
+
return manifest.entities.some(entity => Object.values(entity.fields).some(f => f.type === 'enum'));
|
|
234
|
+
}
|
|
235
|
+
exports.schemaGenerator = {
|
|
236
|
+
name: 'drizzle-schema',
|
|
237
|
+
description: 'Generate Drizzle ORM schema',
|
|
238
|
+
generate(manifest, ctx) {
|
|
239
|
+
const isSqlite = ctx.database.isSqlite;
|
|
240
|
+
const hasEnums = hasEnumFields(manifest);
|
|
241
|
+
// Build imports based on database type and features used
|
|
242
|
+
let imports;
|
|
243
|
+
if (isSqlite) {
|
|
244
|
+
imports = `import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'`;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const pgImports = ['pgTable', 'text', 'integer', 'boolean', 'timestamp'];
|
|
248
|
+
if (hasEnums)
|
|
249
|
+
pgImports.push('pgEnum');
|
|
250
|
+
imports = `import { ${pgImports.join(', ')} } from 'drizzle-orm/pg-core'`;
|
|
251
|
+
}
|
|
252
|
+
// Generate enum definitions for PostgreSQL
|
|
253
|
+
const enumDefs = !isSqlite && hasEnums ? generateEnumDefinitions(manifest) + '\n\n' : '';
|
|
254
|
+
const entities = manifest.entities
|
|
255
|
+
.map(entity => generateEntity(entity, isSqlite, ctx))
|
|
256
|
+
.join('\n\n');
|
|
257
|
+
const junctionTables = generateJunctionTables(manifest, isSqlite, ctx);
|
|
258
|
+
return {
|
|
259
|
+
path: 'db/schema.ts',
|
|
260
|
+
content: `// Auto-generated Drizzle schema
|
|
261
|
+
// Do not edit manually - regenerate with: npx archetype generate
|
|
262
|
+
|
|
263
|
+
${imports}
|
|
264
|
+
|
|
265
|
+
${enumDefs}${entities}
|
|
266
|
+
${junctionTables}
|
|
267
|
+
`,
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Layer Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates service modules for entities backed by external APIs.
|
|
5
|
+
* Only runs when entities have external source configuration.
|
|
6
|
+
*
|
|
7
|
+
* Generated files:
|
|
8
|
+
* - services/apiClient.ts - Reusable HTTP client with typed methods
|
|
9
|
+
* - services/{entity}Service.ts - CRUD operations for each external entity
|
|
10
|
+
* - services/index.ts - Barrel export of all services
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Environment variable support for base URLs (env:VARIABLE_NAME syntax)
|
|
14
|
+
* - RESTful endpoint pattern support (GET /products/:id)
|
|
15
|
+
* - Typed request/response handling
|
|
16
|
+
* - Error handling with HTTP status codes
|
|
17
|
+
* - AbortSignal support for request cancellation
|
|
18
|
+
*
|
|
19
|
+
* @module generators/service
|
|
20
|
+
*/
|
|
21
|
+
import type { Generator } from '../../../template/types';
|
|
22
|
+
export declare const serviceGenerator: Generator;
|
|
23
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../../../src/templates/nextjs-drizzle-trpc/generators/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,yBAAyB,CAAA;AA+QvE,eAAO,MAAM,gBAAgB,EAAE,SAoC9B,CAAA"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Service Layer Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates service modules for entities backed by external APIs.
|
|
6
|
+
* Only runs when entities have external source configuration.
|
|
7
|
+
*
|
|
8
|
+
* Generated files:
|
|
9
|
+
* - services/apiClient.ts - Reusable HTTP client with typed methods
|
|
10
|
+
* - services/{entity}Service.ts - CRUD operations for each external entity
|
|
11
|
+
* - services/index.ts - Barrel export of all services
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Environment variable support for base URLs (env:VARIABLE_NAME syntax)
|
|
15
|
+
* - RESTful endpoint pattern support (GET /products/:id)
|
|
16
|
+
* - Typed request/response handling
|
|
17
|
+
* - Error handling with HTTP status codes
|
|
18
|
+
* - AbortSignal support for request cancellation
|
|
19
|
+
*
|
|
20
|
+
* @module generators/service
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.serviceGenerator = void 0;
|
|
24
|
+
const source_1 = require("../../../source");
|
|
25
|
+
/**
|
|
26
|
+
* Parse environment variable syntax in base URL
|
|
27
|
+
*
|
|
28
|
+
* Converts 'env:VARIABLE_NAME' to 'process.env.VARIABLE_NAME' for runtime resolution.
|
|
29
|
+
* Regular URLs are returned as string literals.
|
|
30
|
+
*
|
|
31
|
+
* @param baseUrl - Base URL string, may contain env: prefix
|
|
32
|
+
* @returns JavaScript expression for the URL
|
|
33
|
+
*/
|
|
34
|
+
function parseEnvUrl(baseUrl) {
|
|
35
|
+
if (baseUrl.startsWith('env:')) {
|
|
36
|
+
const varName = baseUrl.slice(4);
|
|
37
|
+
return `process.env.${varName}`;
|
|
38
|
+
}
|
|
39
|
+
return `'${baseUrl}'`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse endpoint string into HTTP method and path
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* parseEndpoint('GET /products/:id') // { method: 'GET', path: '/products/:id' }
|
|
46
|
+
* parseEndpoint('/products') // { method: 'GET', path: '/products' }
|
|
47
|
+
*
|
|
48
|
+
* @param endpoint - Endpoint string with optional method prefix
|
|
49
|
+
* @returns Object with method and path
|
|
50
|
+
*/
|
|
51
|
+
function parseEndpoint(endpoint) {
|
|
52
|
+
const parts = endpoint.split(' ');
|
|
53
|
+
if (parts.length === 2) {
|
|
54
|
+
return { method: parts[0], path: parts[1] };
|
|
55
|
+
}
|
|
56
|
+
// Default to GET if no method specified
|
|
57
|
+
return { method: 'GET', path: endpoint };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate the shared API client module
|
|
61
|
+
*
|
|
62
|
+
* Creates a reusable HTTP client with typed methods for GET, POST, PUT, DELETE.
|
|
63
|
+
* Includes URL building with query parameters and error handling.
|
|
64
|
+
*
|
|
65
|
+
* @returns Generated file for apiClient.ts
|
|
66
|
+
*/
|
|
67
|
+
function generateApiClient() {
|
|
68
|
+
return {
|
|
69
|
+
path: 'services/apiClient.ts',
|
|
70
|
+
content: `// Auto-generated API client
|
|
71
|
+
// Do not edit manually - regenerate with: npx archetype generate
|
|
72
|
+
|
|
73
|
+
export interface RequestConfig {
|
|
74
|
+
headers?: Record<string, string>
|
|
75
|
+
params?: Record<string, unknown>
|
|
76
|
+
signal?: AbortSignal
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ApiResponse<T> {
|
|
80
|
+
data: T
|
|
81
|
+
status: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class ApiClient {
|
|
85
|
+
private defaultHeaders: Record<string, string>
|
|
86
|
+
|
|
87
|
+
constructor(headers: Record<string, string> = {}) {
|
|
88
|
+
this.defaultHeaders = headers
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private buildUrl(baseUrl: string, path: string, params?: Record<string, unknown>): string {
|
|
92
|
+
const url = new URL(path, baseUrl)
|
|
93
|
+
if (params) {
|
|
94
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
95
|
+
if (value !== undefined && value !== null) {
|
|
96
|
+
url.searchParams.set(key, String(value))
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return url.toString()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async get<T>(baseUrl: string, path: string, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
104
|
+
const url = this.buildUrl(baseUrl, path, config?.params)
|
|
105
|
+
const response = await fetch(url, {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
headers: { ...this.defaultHeaders, ...config?.headers },
|
|
108
|
+
signal: config?.signal,
|
|
109
|
+
})
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(\`HTTP error \${response.status}: \${response.statusText}\`)
|
|
112
|
+
}
|
|
113
|
+
return { data: await response.json(), status: response.status }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async post<T>(baseUrl: string, path: string, body: unknown, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
117
|
+
const response = await fetch(\`\${baseUrl}\${path}\`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
...this.defaultHeaders,
|
|
122
|
+
...config?.headers,
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(body),
|
|
125
|
+
signal: config?.signal,
|
|
126
|
+
})
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(\`HTTP error \${response.status}: \${response.statusText}\`)
|
|
129
|
+
}
|
|
130
|
+
return { data: await response.json(), status: response.status }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async put<T>(baseUrl: string, path: string, body: unknown, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
134
|
+
const response = await fetch(\`\${baseUrl}\${path}\`, {
|
|
135
|
+
method: 'PUT',
|
|
136
|
+
headers: {
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
...this.defaultHeaders,
|
|
139
|
+
...config?.headers,
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
signal: config?.signal,
|
|
143
|
+
})
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(\`HTTP error \${response.status}: \${response.statusText}\`)
|
|
146
|
+
}
|
|
147
|
+
return { data: await response.json(), status: response.status }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async delete<T>(baseUrl: string, path: string, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
151
|
+
const response = await fetch(\`\${baseUrl}\${path}\`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
headers: { ...this.defaultHeaders, ...config?.headers },
|
|
154
|
+
signal: config?.signal,
|
|
155
|
+
})
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error(\`HTTP error \${response.status}: \${response.statusText}\`)
|
|
158
|
+
}
|
|
159
|
+
return { data: await response.json(), status: response.status }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const apiClient = new ApiClient()
|
|
164
|
+
`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generate service module for a single entity with external API source
|
|
169
|
+
*
|
|
170
|
+
* @param entity - Entity IR with source configuration
|
|
171
|
+
* @param source - External source config with base URL and endpoints
|
|
172
|
+
* @param ctx - Generator context
|
|
173
|
+
* @returns Generated file for {entity}Service.ts
|
|
174
|
+
*/
|
|
175
|
+
function generateEntityService(entity, source, ctx) {
|
|
176
|
+
const name = entity.name;
|
|
177
|
+
const lowerName = name.toLowerCase();
|
|
178
|
+
const endpoints = (0, source_1.resolveEndpoints)(name, source);
|
|
179
|
+
const listEndpoint = parseEndpoint(endpoints.list);
|
|
180
|
+
const getEndpoint = parseEndpoint(endpoints.get);
|
|
181
|
+
const createEndpoint = parseEndpoint(endpoints.create);
|
|
182
|
+
const updateEndpoint = parseEndpoint(endpoints.update);
|
|
183
|
+
const deleteEndpoint = parseEndpoint(endpoints.delete);
|
|
184
|
+
const baseUrlCode = parseEnvUrl(source.baseUrl);
|
|
185
|
+
// Generate path with parameter substitution
|
|
186
|
+
const pathWithId = (path) => {
|
|
187
|
+
// Replace :id with ${id}
|
|
188
|
+
return path.replace(/:([a-zA-Z_]+)/g, '${$1}');
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
path: `services/${lowerName}Service.ts`,
|
|
192
|
+
content: `// Auto-generated service for ${name}
|
|
193
|
+
// Do not edit manually - regenerate with: npx archetype generate
|
|
194
|
+
|
|
195
|
+
import { apiClient } from './apiClient'
|
|
196
|
+
import type { ${name}Create, ${name}Update } from '../schemas/${lowerName}'
|
|
197
|
+
|
|
198
|
+
const BASE_URL = ${baseUrlCode}!
|
|
199
|
+
|
|
200
|
+
export interface ${name}ListParams {
|
|
201
|
+
page?: number
|
|
202
|
+
limit?: number
|
|
203
|
+
[key: string]: unknown
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const ${lowerName}Service = {
|
|
207
|
+
/**
|
|
208
|
+
* List all ${name}s
|
|
209
|
+
*/
|
|
210
|
+
async list(params?: ${name}ListParams) {
|
|
211
|
+
const response = await apiClient.get<${name}[]>(BASE_URL, '${listEndpoint.path}', { params })
|
|
212
|
+
return response.data
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get a single ${name} by ID
|
|
217
|
+
*/
|
|
218
|
+
async get(id: string) {
|
|
219
|
+
const response = await apiClient.get<${name}>(BASE_URL, \`${pathWithId(getEndpoint.path)}\`)
|
|
220
|
+
return response.data
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a new ${name}
|
|
225
|
+
*/
|
|
226
|
+
async create(data: ${name}Create) {
|
|
227
|
+
const response = await apiClient.post<${name}>(BASE_URL, '${createEndpoint.path}', data)
|
|
228
|
+
return response.data
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Update an existing ${name}
|
|
233
|
+
*/
|
|
234
|
+
async update(id: string, data: ${name}Update) {
|
|
235
|
+
const response = await apiClient.put<${name}>(BASE_URL, \`${pathWithId(updateEndpoint.path)}\`, data)
|
|
236
|
+
return response.data
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Delete a ${name}
|
|
241
|
+
*/
|
|
242
|
+
async delete(id: string) {
|
|
243
|
+
const response = await apiClient.delete<void>(BASE_URL, \`${pathWithId(deleteEndpoint.path)}\`)
|
|
244
|
+
return response.data
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Type for the entity (inferred from schema)
|
|
249
|
+
export interface ${name} extends ${name}Create {
|
|
250
|
+
id: string
|
|
251
|
+
}
|
|
252
|
+
`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Generate barrel export file for all services
|
|
257
|
+
*
|
|
258
|
+
* @param entities - Entities with external source to include
|
|
259
|
+
* @returns Generated file for services/index.ts
|
|
260
|
+
*/
|
|
261
|
+
function generateServiceIndex(entities) {
|
|
262
|
+
const externalEntities = entities.filter(e => e.source?.type === 'external');
|
|
263
|
+
const exports = externalEntities
|
|
264
|
+
.map(e => `export { ${e.name.toLowerCase()}Service } from './${e.name.toLowerCase()}Service'`)
|
|
265
|
+
.join('\n');
|
|
266
|
+
return {
|
|
267
|
+
path: 'services/index.ts',
|
|
268
|
+
content: `// Auto-generated service exports
|
|
269
|
+
// Do not edit manually - regenerate with: npx archetype generate
|
|
270
|
+
|
|
271
|
+
export { apiClient } from './apiClient'
|
|
272
|
+
${exports}
|
|
273
|
+
`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
exports.serviceGenerator = {
|
|
277
|
+
name: 'service-layer',
|
|
278
|
+
description: 'Generate service layer for external APIs',
|
|
279
|
+
generate(manifest, ctx) {
|
|
280
|
+
const files = [];
|
|
281
|
+
// Collect entities with external source (from entity or manifest level)
|
|
282
|
+
const entitiesWithSource = [];
|
|
283
|
+
for (const entity of manifest.entities) {
|
|
284
|
+
// Entity-level source takes precedence
|
|
285
|
+
const source = entity.source || manifest.source;
|
|
286
|
+
if (source?.type === 'external') {
|
|
287
|
+
entitiesWithSource.push({ entity, source });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Only generate if we have external entities
|
|
291
|
+
if (entitiesWithSource.length === 0) {
|
|
292
|
+
return files;
|
|
293
|
+
}
|
|
294
|
+
// Generate base API client
|
|
295
|
+
files.push(generateApiClient());
|
|
296
|
+
// Generate service for each external entity
|
|
297
|
+
for (const { entity, source } of entitiesWithSource) {
|
|
298
|
+
files.push(generateEntityService(entity, source, ctx));
|
|
299
|
+
}
|
|
300
|
+
// Generate service index
|
|
301
|
+
files.push(generateServiceIndex(entitiesWithSource.map(e => e.entity)));
|
|
302
|
+
return files;
|
|
303
|
+
},
|
|
304
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod Validation Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates Zod validation schemas for entity CRUD operations.
|
|
5
|
+
* Supports internationalized error messages via next-intl.
|
|
6
|
+
*
|
|
7
|
+
* Generated files:
|
|
8
|
+
* - schemas/{entity}.ts - Create and Update schemas with types
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Maps field types to Zod validators (z.string(), z.number(), etc.)
|
|
12
|
+
* - Generates validation rules from field config (min, max, email, url, regex)
|
|
13
|
+
* - Supports i18n error messages when multiple languages configured
|
|
14
|
+
* - Creates both static and i18n-aware schema factory functions
|
|
15
|
+
* - Exports TypeScript types derived from schemas
|
|
16
|
+
*
|
|
17
|
+
* @module generators/validation
|
|
18
|
+
*/
|
|
19
|
+
import type { Generator } from '../../../template/types';
|
|
20
|
+
export declare const validationGenerator: Generator;
|
|
21
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../../../src/templates/nextjs-drizzle-trpc/generators/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAiB,MAAM,yBAAyB,CAAA;AAqOvE,eAAO,MAAM,mBAAmB,EAAE,SAYjC,CAAA"}
|