@compilr-dev/factory 0.1.9 → 0.1.11
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/dist/factory/registry.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/toolkits/react-fastapi/api.d.ts +9 -0
- package/dist/toolkits/react-fastapi/api.js +378 -0
- package/dist/toolkits/react-fastapi/config.d.ts +9 -0
- package/dist/toolkits/react-fastapi/config.js +128 -0
- package/dist/toolkits/react-fastapi/helpers.d.ts +13 -0
- package/dist/toolkits/react-fastapi/helpers.js +54 -0
- package/dist/toolkits/react-fastapi/index.d.ts +11 -0
- package/dist/toolkits/react-fastapi/index.js +55 -0
- package/dist/toolkits/react-fastapi/seed.d.ts +12 -0
- package/dist/toolkits/react-fastapi/seed.js +54 -0
- package/dist/toolkits/react-fastapi/shared.d.ts +6 -0
- package/dist/toolkits/react-fastapi/shared.js +6 -0
- package/dist/toolkits/react-fastapi/static.d.ts +8 -0
- package/dist/toolkits/react-fastapi/static.js +123 -0
- package/dist/toolkits/react-go/api.d.ts +12 -0
- package/dist/toolkits/react-go/api.js +523 -0
- package/dist/toolkits/react-go/config.d.ts +9 -0
- package/dist/toolkits/react-go/config.js +129 -0
- package/dist/toolkits/react-go/helpers.d.ts +13 -0
- package/dist/toolkits/react-go/helpers.js +49 -0
- package/dist/toolkits/react-go/index.d.ts +11 -0
- package/dist/toolkits/react-go/index.js +55 -0
- package/dist/toolkits/react-go/seed.d.ts +12 -0
- package/dist/toolkits/react-go/seed.js +55 -0
- package/dist/toolkits/react-go/shared.d.ts +6 -0
- package/dist/toolkits/react-go/shared.js +6 -0
- package/dist/toolkits/react-go/static.d.ts +8 -0
- package/dist/toolkits/react-go/static.js +122 -0
- package/package.json +1 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/main.go, server/models.go,
|
|
5
|
+
* server/data/{entity}.go, server/handlers/{entity}.go
|
|
6
|
+
*
|
|
7
|
+
* All files use `package main` so `go run ./server` compiles everything.
|
|
8
|
+
* Go 1.22+ net/http stdlib with method-based routing.
|
|
9
|
+
*/
|
|
10
|
+
import { toCamelCase, toKebabCase, toPascalCase } from '../../model/naming.js';
|
|
11
|
+
import { goType, fkFieldName, belongsToRels, hasManyRels } from './helpers.js';
|
|
12
|
+
import { toSnakeCase } from './helpers.js';
|
|
13
|
+
import { generateSeedData } from './seed.js';
|
|
14
|
+
export function generateApiFiles(model) {
|
|
15
|
+
return [
|
|
16
|
+
generateServerMain(model),
|
|
17
|
+
generateModels(model),
|
|
18
|
+
...model.entities.flatMap((entity) => [
|
|
19
|
+
generateDataStore(model, entity),
|
|
20
|
+
generateHandlers(model, entity),
|
|
21
|
+
]),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Server Entry Point — server/main.go
|
|
26
|
+
// =============================================================================
|
|
27
|
+
function generateServerMain(model) {
|
|
28
|
+
const handlerRegistrations = model.entities
|
|
29
|
+
.map((e) => `\tRegister${e.name}Handlers(mux)`)
|
|
30
|
+
.join('\n');
|
|
31
|
+
return {
|
|
32
|
+
path: 'server/main.go',
|
|
33
|
+
content: `package main
|
|
34
|
+
|
|
35
|
+
import (
|
|
36
|
+
\t"fmt"
|
|
37
|
+
\t"log"
|
|
38
|
+
\t"net/http"
|
|
39
|
+
\t"os"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
func corsMiddleware(next http.Handler) http.Handler {
|
|
43
|
+
\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
44
|
+
\t\tw.Header().Set("Access-Control-Allow-Origin", "*")
|
|
45
|
+
\t\tw.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
46
|
+
\t\tw.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
47
|
+
\t\tif r.Method == http.MethodOptions {
|
|
48
|
+
\t\t\tw.WriteHeader(http.StatusNoContent)
|
|
49
|
+
\t\t\treturn
|
|
50
|
+
\t\t}
|
|
51
|
+
\t\tnext.ServeHTTP(w, r)
|
|
52
|
+
\t})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func main() {
|
|
56
|
+
\tmux := http.NewServeMux()
|
|
57
|
+
|
|
58
|
+
${handlerRegistrations}
|
|
59
|
+
|
|
60
|
+
\tport := os.Getenv("PORT")
|
|
61
|
+
\tif port == "" {
|
|
62
|
+
\t\tport = "8080"
|
|
63
|
+
\t}
|
|
64
|
+
|
|
65
|
+
\tfmt.Printf("Server listening on :%s\\n", port)
|
|
66
|
+
\tlog.Fatal(http.ListenAndServe(":"+port, corsMiddleware(mux)))
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Models — server/models.go
|
|
73
|
+
// =============================================================================
|
|
74
|
+
function generateModels(model) {
|
|
75
|
+
const modelBlocks = [];
|
|
76
|
+
for (const entity of model.entities) {
|
|
77
|
+
const btoRels = belongsToRels(entity);
|
|
78
|
+
// Main struct (with json tags)
|
|
79
|
+
const structFields = [];
|
|
80
|
+
structFields.push(`\tID int \`json:"id"\``);
|
|
81
|
+
for (const field of entity.fields) {
|
|
82
|
+
const goT = goType(field);
|
|
83
|
+
const jsonTag = field.name;
|
|
84
|
+
const goFieldName = toPascalCase(field.name);
|
|
85
|
+
structFields.push(`\t${goFieldName} ${goT} \`json:"${jsonTag}"\``);
|
|
86
|
+
}
|
|
87
|
+
for (const rel of btoRels) {
|
|
88
|
+
const fk = fkFieldName(rel);
|
|
89
|
+
const goFieldName = toPascalCase(fk);
|
|
90
|
+
structFields.push(`\t${goFieldName} int \`json:"${fk}"\``);
|
|
91
|
+
}
|
|
92
|
+
structFields.push(`\tCreatedAt string \`json:"createdAt"\``);
|
|
93
|
+
structFields.push(`\tUpdatedAt string \`json:"updatedAt"\``);
|
|
94
|
+
// Input struct (for create/update — pointer fields for optional update detection)
|
|
95
|
+
const inputFields = [];
|
|
96
|
+
for (const field of entity.fields) {
|
|
97
|
+
const goT = goType(field);
|
|
98
|
+
const jsonTag = field.name;
|
|
99
|
+
const goFieldName = toPascalCase(field.name);
|
|
100
|
+
inputFields.push(`\t${goFieldName} *${goT} \`json:"${jsonTag}"\``);
|
|
101
|
+
}
|
|
102
|
+
for (const rel of btoRels) {
|
|
103
|
+
const fk = fkFieldName(rel);
|
|
104
|
+
const goFieldName = toPascalCase(fk);
|
|
105
|
+
inputFields.push(`\t${goFieldName} *int \`json:"${fk}"\``);
|
|
106
|
+
}
|
|
107
|
+
modelBlocks.push(`type ${entity.name} struct {
|
|
108
|
+
${structFields.join('\n')}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type ${entity.name}Input struct {
|
|
112
|
+
${inputFields.join('\n')}
|
|
113
|
+
}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
path: 'server/models.go',
|
|
117
|
+
content: `package main
|
|
118
|
+
${modelBlocks.length > 0 ? '\n' + modelBlocks.join('\n\n') + '\n' : ''}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// Data Store — server/data/{entity}.go
|
|
123
|
+
// =============================================================================
|
|
124
|
+
function generateDataStore(model, entity) {
|
|
125
|
+
const lowerName = toCamelCase(entity.pluralName);
|
|
126
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
127
|
+
// Collect searchable fields (string + enum types)
|
|
128
|
+
const searchableFields = entity.fields
|
|
129
|
+
.filter((f) => f.type === 'string' || f.type === 'enum')
|
|
130
|
+
.map((f) => toPascalCase(f.name));
|
|
131
|
+
let searchLogic;
|
|
132
|
+
if (searchableFields.length > 0) {
|
|
133
|
+
const fieldChecks = searchableFields
|
|
134
|
+
.map((f) => `\t\t\t\tstrings.Contains(strings.ToLower(fmt.Sprintf("%v", item.${f})), q)`)
|
|
135
|
+
.join(' ||\n');
|
|
136
|
+
searchLogic = `\t\tq := strings.ToLower(search)
|
|
137
|
+
\t\tvar filtered []${entity.name}
|
|
138
|
+
\t\tfor _, item := range result {
|
|
139
|
+
\t\t\tif ${fieldChecks.trim()} {
|
|
140
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
141
|
+
\t\t\t}
|
|
142
|
+
\t\t}
|
|
143
|
+
\t\tresult = filtered`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
searchLogic = `\t\tq := strings.ToLower(search)
|
|
147
|
+
\t\tvar filtered []${entity.name}
|
|
148
|
+
\t\tfor _, item := range result {
|
|
149
|
+
\t\t\tif strings.Contains(strings.ToLower(fmt.Sprintf("%v", item)), q) {
|
|
150
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
151
|
+
\t\t\t}
|
|
152
|
+
\t\t}
|
|
153
|
+
\t\tresult = filtered`;
|
|
154
|
+
}
|
|
155
|
+
// Filter logic — check field values via reflect-like approach (simple string comparison)
|
|
156
|
+
const filterableFields = entity.fields.map((f) => ({
|
|
157
|
+
jsonName: f.name,
|
|
158
|
+
goName: toPascalCase(f.name),
|
|
159
|
+
}));
|
|
160
|
+
const filterChecks = filterableFields
|
|
161
|
+
.map((f) => `\t\t\t\tcase "${f.jsonName}":\n\t\t\t\t\tif fmt.Sprintf("%v", item.${f.goName}) != value {\n\t\t\t\t\t\tmatch = false\n\t\t\t\t\t}`)
|
|
162
|
+
.join('\n');
|
|
163
|
+
const seedData = generateSeedData(model, entity);
|
|
164
|
+
return {
|
|
165
|
+
path: `server/data/${snakeName}.go`,
|
|
166
|
+
content: `package main
|
|
167
|
+
|
|
168
|
+
import (
|
|
169
|
+
\t"fmt"
|
|
170
|
+
\t"strings"
|
|
171
|
+
\t"sync"
|
|
172
|
+
\t"time"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
${seedData}
|
|
176
|
+
|
|
177
|
+
type ${entity.name}Store struct {
|
|
178
|
+
\tmu sync.Mutex
|
|
179
|
+
\titems []${entity.name}
|
|
180
|
+
\tnextID int
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var ${lowerName}Store = &${entity.name}Store{
|
|
184
|
+
\titems: make([]${entity.name}, len(seedData)),
|
|
185
|
+
\tnextID: len(seedData) + 1,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func init() {
|
|
189
|
+
\tcopy(${lowerName}Store.items, seedData)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func (s *${entity.name}Store) GetAll() []${entity.name} {
|
|
193
|
+
\ts.mu.Lock()
|
|
194
|
+
\tdefer s.mu.Unlock()
|
|
195
|
+
\tresult := make([]${entity.name}, len(s.items))
|
|
196
|
+
\tcopy(result, s.items)
|
|
197
|
+
\treturn result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func (s *${entity.name}Store) GetByID(id int) (${entity.name}, bool) {
|
|
201
|
+
\ts.mu.Lock()
|
|
202
|
+
\tdefer s.mu.Unlock()
|
|
203
|
+
\tfor _, item := range s.items {
|
|
204
|
+
\t\tif item.ID == id {
|
|
205
|
+
\t\t\treturn item, true
|
|
206
|
+
\t\t}
|
|
207
|
+
\t}
|
|
208
|
+
\treturn ${entity.name}{}, false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func (s *${entity.name}Store) Query(search string, filters map[string]string) ([]${entity.name}, int) {
|
|
212
|
+
\ts.mu.Lock()
|
|
213
|
+
\tdefer s.mu.Unlock()
|
|
214
|
+
\tresult := make([]${entity.name}, len(s.items))
|
|
215
|
+
\tcopy(result, s.items)
|
|
216
|
+
|
|
217
|
+
\tif search != "" {
|
|
218
|
+
${searchLogic}
|
|
219
|
+
\t}
|
|
220
|
+
|
|
221
|
+
\tif len(filters) > 0 {
|
|
222
|
+
\t\tvar filtered []${entity.name}
|
|
223
|
+
\t\tfor _, item := range result {
|
|
224
|
+
\t\t\tmatch := true
|
|
225
|
+
\t\t\tfor field, value := range filters {
|
|
226
|
+
\t\t\t\tswitch field {
|
|
227
|
+
${filterChecks}
|
|
228
|
+
\t\t\t\t}
|
|
229
|
+
\t\t\t}
|
|
230
|
+
\t\t\tif match {
|
|
231
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
232
|
+
\t\t\t}
|
|
233
|
+
\t\t}
|
|
234
|
+
\t\tresult = filtered
|
|
235
|
+
\t}
|
|
236
|
+
|
|
237
|
+
\treturn result, len(result)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func (s *${entity.name}Store) Create(input ${entity.name}Input) ${entity.name} {
|
|
241
|
+
\ts.mu.Lock()
|
|
242
|
+
\tdefer s.mu.Unlock()
|
|
243
|
+
\tnow := time.Now().UTC().Format(time.RFC3339)
|
|
244
|
+
\titem := ${entity.name}{
|
|
245
|
+
\t\tID: s.nextID,
|
|
246
|
+
\t\tCreatedAt: now,
|
|
247
|
+
\t\tUpdatedAt: now,
|
|
248
|
+
\t}
|
|
249
|
+
\ts.nextID++
|
|
250
|
+
${entity.fields.map((f) => `\tif input.${toPascalCase(f.name)} != nil {\n\t\titem.${toPascalCase(f.name)} = *input.${toPascalCase(f.name)}\n\t}`).join('\n')}
|
|
251
|
+
${belongsToRels(entity)
|
|
252
|
+
.map((rel) => {
|
|
253
|
+
const fk = fkFieldName(rel);
|
|
254
|
+
const goField = toPascalCase(fk);
|
|
255
|
+
return `\tif input.${goField} != nil {\n\t\titem.${goField} = *input.${goField}\n\t}`;
|
|
256
|
+
})
|
|
257
|
+
.join('\n')}
|
|
258
|
+
\ts.items = append(s.items, item)
|
|
259
|
+
\treturn item
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func (s *${entity.name}Store) Update(id int, input ${entity.name}Input) (${entity.name}, bool) {
|
|
263
|
+
\ts.mu.Lock()
|
|
264
|
+
\tdefer s.mu.Unlock()
|
|
265
|
+
\tfor i, item := range s.items {
|
|
266
|
+
\t\tif item.ID == id {
|
|
267
|
+
\t\t\tnow := time.Now().UTC().Format(time.RFC3339)
|
|
268
|
+
${entity.fields.map((f) => `\t\t\tif input.${toPascalCase(f.name)} != nil {\n\t\t\t\ts.items[i].${toPascalCase(f.name)} = *input.${toPascalCase(f.name)}\n\t\t\t}`).join('\n')}
|
|
269
|
+
${belongsToRels(entity)
|
|
270
|
+
.map((rel) => {
|
|
271
|
+
const fk = fkFieldName(rel);
|
|
272
|
+
const goField = toPascalCase(fk);
|
|
273
|
+
return `\t\t\tif input.${goField} != nil {\n\t\t\t\ts.items[i].${goField} = *input.${goField}\n\t\t\t}`;
|
|
274
|
+
})
|
|
275
|
+
.join('\n')}
|
|
276
|
+
\t\t\ts.items[i].UpdatedAt = now
|
|
277
|
+
\t\t\treturn s.items[i], true
|
|
278
|
+
\t\t}
|
|
279
|
+
\t}
|
|
280
|
+
\treturn ${entity.name}{}, false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func (s *${entity.name}Store) Remove(id int) bool {
|
|
284
|
+
\ts.mu.Lock()
|
|
285
|
+
\tdefer s.mu.Unlock()
|
|
286
|
+
\tfor i, item := range s.items {
|
|
287
|
+
\t\tif item.ID == id {
|
|
288
|
+
\t\t\ts.items = append(s.items[:i], s.items[i+1:]...)
|
|
289
|
+
\t\t\treturn true
|
|
290
|
+
\t\t}
|
|
291
|
+
\t}
|
|
292
|
+
\treturn false
|
|
293
|
+
}
|
|
294
|
+
`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// Handlers — server/handlers/{entity}.go
|
|
299
|
+
// =============================================================================
|
|
300
|
+
function generateHandlers(model, entity) {
|
|
301
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
302
|
+
const lowerName = toCamelCase(entity.pluralName);
|
|
303
|
+
const apiRoute = '/api/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
304
|
+
const btoRels = belongsToRels(entity);
|
|
305
|
+
const hmRels = hasManyRels(entity);
|
|
306
|
+
// Populate function for belongsTo
|
|
307
|
+
let populateType = '';
|
|
308
|
+
let populateFunc = '';
|
|
309
|
+
if (btoRels.length > 0) {
|
|
310
|
+
const populateFields = [];
|
|
311
|
+
// Original entity fields
|
|
312
|
+
populateFields.push(`\t${entity.name}`);
|
|
313
|
+
for (const rel of btoRels) {
|
|
314
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
315
|
+
if (!targetEntity)
|
|
316
|
+
continue;
|
|
317
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
318
|
+
populateFields.push(`\t${toPascalCase(targetVar)} *${rel.target} \`json:"${targetVar}"\``);
|
|
319
|
+
}
|
|
320
|
+
populateType = `\ntype ${entity.name}WithRels struct {
|
|
321
|
+
${populateFields.join('\n')}
|
|
322
|
+
}\n`;
|
|
323
|
+
const populateBody = btoRels
|
|
324
|
+
.map((rel) => {
|
|
325
|
+
const fk = fkFieldName(rel);
|
|
326
|
+
const goField = toPascalCase(fk);
|
|
327
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
328
|
+
if (!targetEntity)
|
|
329
|
+
return '';
|
|
330
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
331
|
+
const targetStoreName = toCamelCase(targetEntity.pluralName);
|
|
332
|
+
return `\tif item.${goField} != 0 {
|
|
333
|
+
\t\tif found, ok := ${targetStoreName}Store.GetByID(item.${goField}); ok {
|
|
334
|
+
\t\t\tresult.${toPascalCase(targetVar)} = &found
|
|
335
|
+
\t\t}
|
|
336
|
+
\t}`;
|
|
337
|
+
})
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.join('\n');
|
|
340
|
+
populateFunc = `
|
|
341
|
+
func populate${entity.name}(item ${entity.name}) ${entity.name}WithRels {
|
|
342
|
+
\tresult := ${entity.name}WithRels{${entity.name}: item}
|
|
343
|
+
${populateBody}
|
|
344
|
+
\treturn result
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
// hasMany include logic
|
|
349
|
+
let includeLogic = '';
|
|
350
|
+
let includeType = '';
|
|
351
|
+
if (hmRels.length > 0) {
|
|
352
|
+
const includeFields = [];
|
|
353
|
+
if (btoRels.length > 0) {
|
|
354
|
+
includeFields.push(`\t${entity.name}WithRels`);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
includeFields.push(`\t${entity.name}`);
|
|
358
|
+
}
|
|
359
|
+
for (const rel of hmRels) {
|
|
360
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
361
|
+
if (!targetEntity)
|
|
362
|
+
continue;
|
|
363
|
+
const targetVar = toCamelCase(targetEntity.pluralName);
|
|
364
|
+
includeFields.push(`\t${toPascalCase(targetVar)} []${rel.target} \`json:"${targetVar},omitempty"\``);
|
|
365
|
+
}
|
|
366
|
+
includeType = `\ntype ${entity.name}WithIncludes struct {
|
|
367
|
+
${includeFields.join('\n')}
|
|
368
|
+
}\n`;
|
|
369
|
+
const includeChecks = hmRels
|
|
370
|
+
.map((rel) => {
|
|
371
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
372
|
+
if (!targetEntity)
|
|
373
|
+
return '';
|
|
374
|
+
const targetVar = toCamelCase(targetEntity.pluralName);
|
|
375
|
+
const targetStoreName = toCamelCase(targetEntity.pluralName);
|
|
376
|
+
const fkRel = targetEntity.relationships.find((r) => r.type === 'belongsTo' && r.target === entity.name) ?? { type: 'belongsTo', target: entity.name };
|
|
377
|
+
const fk = fkFieldName(fkRel);
|
|
378
|
+
const goFk = toPascalCase(fk);
|
|
379
|
+
return `\t\tfor _, inc := range includes {
|
|
380
|
+
\t\t\tif inc == "${targetVar}" {
|
|
381
|
+
\t\t\t\tallItems := ${targetStoreName}Store.GetAll()
|
|
382
|
+
\t\t\t\tvar related []${rel.target}
|
|
383
|
+
\t\t\t\tfor _, r := range allItems {
|
|
384
|
+
\t\t\t\t\tif r.${goFk} == item.ID {
|
|
385
|
+
\t\t\t\t\t\trelated = append(related, r)
|
|
386
|
+
\t\t\t\t\t}
|
|
387
|
+
\t\t\t\t}
|
|
388
|
+
\t\t\t\tresult.${toPascalCase(targetVar)} = related
|
|
389
|
+
\t\t\t}
|
|
390
|
+
\t\t}`;
|
|
391
|
+
})
|
|
392
|
+
.filter(Boolean)
|
|
393
|
+
.join('\n');
|
|
394
|
+
includeLogic = includeChecks;
|
|
395
|
+
}
|
|
396
|
+
// GET list handler
|
|
397
|
+
const listReturnExpr = btoRels.length > 0
|
|
398
|
+
? `\tpopulated := make([]${entity.name}WithRels, len(items))
|
|
399
|
+
\tfor i, item := range items {
|
|
400
|
+
\t\tpopulated[i] = populate${entity.name}(item)
|
|
401
|
+
\t}
|
|
402
|
+
\tjson.NewEncoder(w).Encode(map[string]any{"data": populated, "total": total})`
|
|
403
|
+
: `\tjson.NewEncoder(w).Encode(map[string]any{"data": items, "total": total})`;
|
|
404
|
+
// GET by ID handler
|
|
405
|
+
let getByIDBody;
|
|
406
|
+
if (hmRels.length > 0) {
|
|
407
|
+
const baseExpr = btoRels.length > 0 ? `populate${entity.name}(item)` : `item`;
|
|
408
|
+
const innerType = btoRels.length > 0 ? `${entity.name}WithRels` : entity.name;
|
|
409
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
410
|
+
\tif !ok {
|
|
411
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
412
|
+
\t\treturn
|
|
413
|
+
\t}
|
|
414
|
+
\tincludeParam := r.URL.Query().Get("include")
|
|
415
|
+
\tincludes := strings.Split(includeParam, ",")
|
|
416
|
+
\tresult := ${entity.name}WithIncludes{${innerType}: ${baseExpr}}
|
|
417
|
+
${includeLogic}
|
|
418
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
419
|
+
\tjson.NewEncoder(w).Encode(result)`;
|
|
420
|
+
}
|
|
421
|
+
else if (btoRels.length > 0) {
|
|
422
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
423
|
+
\tif !ok {
|
|
424
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
425
|
+
\t\treturn
|
|
426
|
+
\t}
|
|
427
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
428
|
+
\tjson.NewEncoder(w).Encode(populate${entity.name}(item))`;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
432
|
+
\tif !ok {
|
|
433
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
434
|
+
\t\treturn
|
|
435
|
+
\t}
|
|
436
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
437
|
+
\tjson.NewEncoder(w).Encode(item)`;
|
|
438
|
+
}
|
|
439
|
+
// Determine imports needed
|
|
440
|
+
const needsStrings = hmRels.length > 0;
|
|
441
|
+
return {
|
|
442
|
+
path: `server/handlers/${snakeName}.go`,
|
|
443
|
+
content: `package main
|
|
444
|
+
|
|
445
|
+
import (
|
|
446
|
+
\t"encoding/json"
|
|
447
|
+
\t"net/http"
|
|
448
|
+
\t"strconv"${needsStrings ? '\n\t"strings"' : ''}
|
|
449
|
+
)
|
|
450
|
+
${populateType}${includeType}${populateFunc}
|
|
451
|
+
func Register${entity.name}Handlers(mux *http.ServeMux) {
|
|
452
|
+
\tmux.HandleFunc("GET ${apiRoute}", func(w http.ResponseWriter, r *http.Request) {
|
|
453
|
+
\t\tsearch := r.URL.Query().Get("search")
|
|
454
|
+
\t\tfilters := make(map[string]string)
|
|
455
|
+
\t\tfor key, values := range r.URL.Query() {
|
|
456
|
+
\t\t\tif len(key) > 7 && key[:7] == "filter[" && key[len(key)-1] == ']' {
|
|
457
|
+
\t\t\t\tfield := key[7 : len(key)-1]
|
|
458
|
+
\t\t\t\tfilters[field] = values[0]
|
|
459
|
+
\t\t\t}
|
|
460
|
+
\t\t}
|
|
461
|
+
\t\titems, total := ${lowerName}Store.Query(search, filters)
|
|
462
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
463
|
+
${listReturnExpr}
|
|
464
|
+
\t})
|
|
465
|
+
|
|
466
|
+
\tmux.HandleFunc("GET ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
467
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
468
|
+
\t\tif err != nil {
|
|
469
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
470
|
+
\t\t\treturn
|
|
471
|
+
\t\t}
|
|
472
|
+
${getByIDBody}
|
|
473
|
+
\t})
|
|
474
|
+
|
|
475
|
+
\tmux.HandleFunc("POST ${apiRoute}", func(w http.ResponseWriter, r *http.Request) {
|
|
476
|
+
\t\tvar input ${entity.name}Input
|
|
477
|
+
\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
478
|
+
\t\t\thttp.Error(w, \`{"error":"invalid json"}\`, http.StatusBadRequest)
|
|
479
|
+
\t\t\treturn
|
|
480
|
+
\t\t}
|
|
481
|
+
\t\titem := ${lowerName}Store.Create(input)
|
|
482
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
483
|
+
\t\tw.WriteHeader(http.StatusCreated)
|
|
484
|
+
\t\tjson.NewEncoder(w).Encode(item)
|
|
485
|
+
\t})
|
|
486
|
+
|
|
487
|
+
\tmux.HandleFunc("PUT ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
488
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
489
|
+
\t\tif err != nil {
|
|
490
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
491
|
+
\t\t\treturn
|
|
492
|
+
\t\t}
|
|
493
|
+
\t\tvar input ${entity.name}Input
|
|
494
|
+
\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
495
|
+
\t\t\thttp.Error(w, \`{"error":"invalid json"}\`, http.StatusBadRequest)
|
|
496
|
+
\t\t\treturn
|
|
497
|
+
\t\t}
|
|
498
|
+
\t\titem, ok := ${lowerName}Store.Update(id, input)
|
|
499
|
+
\t\tif !ok {
|
|
500
|
+
\t\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
501
|
+
\t\t\treturn
|
|
502
|
+
\t\t}
|
|
503
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
504
|
+
\t\tjson.NewEncoder(w).Encode(item)
|
|
505
|
+
\t})
|
|
506
|
+
|
|
507
|
+
\tmux.HandleFunc("DELETE ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
508
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
509
|
+
\t\tif err != nil {
|
|
510
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
511
|
+
\t\t\treturn
|
|
512
|
+
\t\t}
|
|
513
|
+
\t\tok := ${lowerName}Store.Remove(id)
|
|
514
|
+
\t\tif !ok {
|
|
515
|
+
\t\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
516
|
+
\t\t\treturn
|
|
517
|
+
\t\t}
|
|
518
|
+
\t\tw.WriteHeader(http.StatusNoContent)
|
|
519
|
+
\t})
|
|
520
|
+
}
|
|
521
|
+
`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, go.mod
|
|
6
|
+
*/
|
|
7
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
8
|
+
import type { FactoryFile } from '../types.js';
|
|
9
|
+
export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, go.mod
|
|
6
|
+
*/
|
|
7
|
+
import { toKebabCase } from '../../model/naming.js';
|
|
8
|
+
import { generateColorShades } from '../shared/color-utils.js';
|
|
9
|
+
export function generateConfigFiles(model) {
|
|
10
|
+
const appSlug = toKebabCase(model.identity.name);
|
|
11
|
+
return [
|
|
12
|
+
generatePackageJson(appSlug),
|
|
13
|
+
generateViteConfig(),
|
|
14
|
+
generateTailwindConfig(model),
|
|
15
|
+
generateTsConfig(),
|
|
16
|
+
generatePostCssConfig(),
|
|
17
|
+
generateGoMod(appSlug),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
function generatePackageJson(appSlug) {
|
|
21
|
+
const pkg = {
|
|
22
|
+
name: appSlug,
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
private: true,
|
|
25
|
+
type: 'module',
|
|
26
|
+
scripts: {
|
|
27
|
+
dev: 'concurrently "vite" "go run ./server"',
|
|
28
|
+
build: 'vite build',
|
|
29
|
+
preview: 'vite preview',
|
|
30
|
+
server: 'go run ./server',
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
react: '^18.3.1',
|
|
34
|
+
'react-dom': '^18.3.1',
|
|
35
|
+
'react-router-dom': '^6.28.0',
|
|
36
|
+
},
|
|
37
|
+
devDependencies: {
|
|
38
|
+
'@types/react': '^18.3.12',
|
|
39
|
+
'@types/react-dom': '^18.3.1',
|
|
40
|
+
'@vitejs/plugin-react': '^4.3.4',
|
|
41
|
+
autoprefixer: '^10.4.20',
|
|
42
|
+
concurrently: '^9.1.0',
|
|
43
|
+
postcss: '^8.4.49',
|
|
44
|
+
'@tailwindcss/forms': '^0.5.9',
|
|
45
|
+
tailwindcss: '^3.4.15',
|
|
46
|
+
typescript: '^5.6.3',
|
|
47
|
+
vite: '^6.0.0',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return { path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' };
|
|
51
|
+
}
|
|
52
|
+
function generateViteConfig() {
|
|
53
|
+
return {
|
|
54
|
+
path: 'vite.config.ts',
|
|
55
|
+
content: `import { defineConfig } from 'vite';
|
|
56
|
+
import react from '@vitejs/plugin-react';
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
plugins: [react()],
|
|
60
|
+
server: {
|
|
61
|
+
proxy: {
|
|
62
|
+
'/api': 'http://localhost:8080',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function generateTailwindConfig(model) {
|
|
70
|
+
return {
|
|
71
|
+
path: 'tailwind.config.js',
|
|
72
|
+
content: `import forms from '@tailwindcss/forms';
|
|
73
|
+
|
|
74
|
+
/** @type {import('tailwindcss').Config} */
|
|
75
|
+
export default {
|
|
76
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
77
|
+
darkMode: 'class',
|
|
78
|
+
theme: {
|
|
79
|
+
extend: {
|
|
80
|
+
colors: {
|
|
81
|
+
primary: ${generateColorShades(model.theme.primaryColor)},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
plugins: [forms],
|
|
86
|
+
};
|
|
87
|
+
`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function generateTsConfig() {
|
|
91
|
+
const config = {
|
|
92
|
+
compilerOptions: {
|
|
93
|
+
target: 'ES2022',
|
|
94
|
+
module: 'ESNext',
|
|
95
|
+
moduleResolution: 'bundler',
|
|
96
|
+
jsx: 'react-jsx',
|
|
97
|
+
strict: true,
|
|
98
|
+
esModuleInterop: true,
|
|
99
|
+
skipLibCheck: true,
|
|
100
|
+
forceConsistentCasingInFileNames: true,
|
|
101
|
+
resolveJsonModule: true,
|
|
102
|
+
isolatedModules: true,
|
|
103
|
+
noEmit: true,
|
|
104
|
+
},
|
|
105
|
+
include: ['src'],
|
|
106
|
+
};
|
|
107
|
+
return { path: 'tsconfig.json', content: JSON.stringify(config, null, 2) + '\n' };
|
|
108
|
+
}
|
|
109
|
+
function generatePostCssConfig() {
|
|
110
|
+
return {
|
|
111
|
+
path: 'postcss.config.js',
|
|
112
|
+
content: `export default {
|
|
113
|
+
plugins: {
|
|
114
|
+
tailwindcss: {},
|
|
115
|
+
autoprefixer: {},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function generateGoMod(appSlug) {
|
|
122
|
+
return {
|
|
123
|
+
path: 'go.mod',
|
|
124
|
+
content: `module ${appSlug}
|
|
125
|
+
|
|
126
|
+
go 1.22
|
|
127
|
+
`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Helpers
|
|
3
|
+
*
|
|
4
|
+
* Go-specific helpers plus re-exports from shared helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Field } from '../../model/types.js';
|
|
7
|
+
/** Map a model FieldType to a Go type string. */
|
|
8
|
+
export declare function goType(field: Field): string;
|
|
9
|
+
/** Generate a Go zero value string for a field. */
|
|
10
|
+
export declare function goZeroValue(field: Field): string;
|
|
11
|
+
/** Convert PascalCase or camelCase to snake_case. */
|
|
12
|
+
export declare function toSnakeCase(name: string): string;
|
|
13
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|