@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.
Files changed (32) hide show
  1. package/dist/factory/registry.js +4 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +4 -0
  4. package/dist/toolkits/react-fastapi/api.d.ts +9 -0
  5. package/dist/toolkits/react-fastapi/api.js +378 -0
  6. package/dist/toolkits/react-fastapi/config.d.ts +9 -0
  7. package/dist/toolkits/react-fastapi/config.js +128 -0
  8. package/dist/toolkits/react-fastapi/helpers.d.ts +13 -0
  9. package/dist/toolkits/react-fastapi/helpers.js +54 -0
  10. package/dist/toolkits/react-fastapi/index.d.ts +11 -0
  11. package/dist/toolkits/react-fastapi/index.js +55 -0
  12. package/dist/toolkits/react-fastapi/seed.d.ts +12 -0
  13. package/dist/toolkits/react-fastapi/seed.js +54 -0
  14. package/dist/toolkits/react-fastapi/shared.d.ts +6 -0
  15. package/dist/toolkits/react-fastapi/shared.js +6 -0
  16. package/dist/toolkits/react-fastapi/static.d.ts +8 -0
  17. package/dist/toolkits/react-fastapi/static.js +123 -0
  18. package/dist/toolkits/react-go/api.d.ts +12 -0
  19. package/dist/toolkits/react-go/api.js +523 -0
  20. package/dist/toolkits/react-go/config.d.ts +9 -0
  21. package/dist/toolkits/react-go/config.js +129 -0
  22. package/dist/toolkits/react-go/helpers.d.ts +13 -0
  23. package/dist/toolkits/react-go/helpers.js +49 -0
  24. package/dist/toolkits/react-go/index.d.ts +11 -0
  25. package/dist/toolkits/react-go/index.js +55 -0
  26. package/dist/toolkits/react-go/seed.d.ts +12 -0
  27. package/dist/toolkits/react-go/seed.js +55 -0
  28. package/dist/toolkits/react-go/shared.d.ts +6 -0
  29. package/dist/toolkits/react-go/shared.js +6 -0
  30. package/dist/toolkits/react-go/static.d.ts +8 -0
  31. package/dist/toolkits/react-go/static.js +122 -0
  32. 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';