go-duck-cli 1.0.5 → 1.0.6
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/generators/postgrest.js +32 -10
- package/generators/swagger.js +118 -16
- package/package.json +1 -1
- package/templates/docs/pages/rest.hbs +78 -0
- package/templates/go/controller.go.hbs +98 -0
- package/templates/go/main.go.hbs +3 -0
package/generators/postgrest.js
CHANGED
|
@@ -35,11 +35,30 @@ func (sc *SearchController) GenericSearch(c *gin.Context) {
|
|
|
35
35
|
continue
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Security: Basic Sanitization for Key (Allowing letters, numbers, _, -, and JSON arrows)
|
|
39
|
+
// We split the key if it contains JSON operators to handle them specifically
|
|
40
|
+
processedKey := key
|
|
41
|
+
if strings.Contains(key, "->") {
|
|
42
|
+
parts := strings.SplitN(key, "->", 2)
|
|
43
|
+
column := parts[0]
|
|
44
|
+
path := parts[1]
|
|
45
|
+
operator := "->"
|
|
46
|
+
if strings.HasPrefix(path, ">") {
|
|
47
|
+
operator = "->>"
|
|
48
|
+
path = path[1:]
|
|
49
|
+
}
|
|
50
|
+
// Wrap column in quotes and path in single quotes for Postgres JSONB safety
|
|
51
|
+
processedKey = fmt.Sprintf("\"%s\"%s'%s'", column, operator, path)
|
|
52
|
+
} else {
|
|
53
|
+
// Standard column: Wrap in quotes for safety
|
|
54
|
+
processedKey = fmt.Sprintf("\"%s\"", key)
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
for _, val := range values {
|
|
39
58
|
parts := strings.SplitN(val, ".", 2)
|
|
40
59
|
if len(parts) < 2 {
|
|
41
60
|
// Default to equality
|
|
42
|
-
query = query.Where(
|
|
61
|
+
query = query.Where(processedKey+" = ?", val)
|
|
43
62
|
continue
|
|
44
63
|
}
|
|
45
64
|
|
|
@@ -48,24 +67,27 @@ func (sc *SearchController) GenericSearch(c *gin.Context) {
|
|
|
48
67
|
|
|
49
68
|
switch op {
|
|
50
69
|
case "eq":
|
|
51
|
-
query = query.Where(
|
|
70
|
+
query = query.Where(processedKey+" = ?", target)
|
|
52
71
|
case "neq":
|
|
53
|
-
query = query.Where(
|
|
72
|
+
query = query.Where(processedKey+" <> ?", target)
|
|
54
73
|
case "gt":
|
|
55
|
-
query = query.Where(
|
|
74
|
+
query = query.Where(processedKey+" > ?", target)
|
|
56
75
|
case "gte":
|
|
57
|
-
query = query.Where(
|
|
76
|
+
query = query.Where(processedKey+" >= ?", target)
|
|
58
77
|
case "lt":
|
|
59
|
-
query = query.Where(
|
|
78
|
+
query = query.Where(processedKey+" < ?", target)
|
|
60
79
|
case "lte":
|
|
61
|
-
query = query.Where(
|
|
80
|
+
query = query.Where(processedKey+" <= ?", target)
|
|
62
81
|
case "like":
|
|
63
|
-
query = query.Where(
|
|
82
|
+
query = query.Where(processedKey+" LIKE ?", "%"+target+"%")
|
|
64
83
|
case "ilike":
|
|
65
|
-
query = query.Where(
|
|
84
|
+
query = query.Where(processedKey+" ILIKE ?", "%"+target+"%")
|
|
66
85
|
case "in":
|
|
67
86
|
list := strings.Split(target, ",")
|
|
68
|
-
query = query.Where(
|
|
87
|
+
query = query.Where(processedKey+" IN ?", list)
|
|
88
|
+
default:
|
|
89
|
+
// Fallback to equality if operator is unrecognized
|
|
90
|
+
query = query.Where(processedKey+" = ?", val)
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
}
|
package/generators/swagger.js
CHANGED
|
@@ -18,6 +18,19 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
18
18
|
],
|
|
19
19
|
paths: {},
|
|
20
20
|
components: {
|
|
21
|
+
securitySchemes: {
|
|
22
|
+
BearerAuth: {
|
|
23
|
+
type: 'http',
|
|
24
|
+
scheme: 'bearer',
|
|
25
|
+
bearerFormat: 'JWT'
|
|
26
|
+
},
|
|
27
|
+
TenantID: {
|
|
28
|
+
type: 'apiKey',
|
|
29
|
+
in: 'header',
|
|
30
|
+
name: 'X-Tenant-ID',
|
|
31
|
+
description: 'The unique identifier for the tenant dashboard context'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
21
34
|
schemas: {
|
|
22
35
|
Error: {
|
|
23
36
|
type: 'object',
|
|
@@ -26,9 +39,16 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
}
|
|
29
|
-
}
|
|
42
|
+
},
|
|
43
|
+
security: [
|
|
44
|
+
{ BearerAuth: [], TenantID: [] }
|
|
45
|
+
]
|
|
30
46
|
};
|
|
31
47
|
|
|
48
|
+
const commonHeaders = [
|
|
49
|
+
{ name: 'X-Tenant-ID', in: 'header', required: true, schema: { type: 'string', default: 'default' }, description: 'Multi-tenancy context identifier' }
|
|
50
|
+
];
|
|
51
|
+
|
|
32
52
|
// 1. Add Entity Paths
|
|
33
53
|
for (const entity of entities) {
|
|
34
54
|
const name = entity.name.toLowerCase();
|
|
@@ -53,6 +73,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
53
73
|
post: {
|
|
54
74
|
tags: [capitalized],
|
|
55
75
|
summary: `Create a new ${capitalized}`,
|
|
76
|
+
parameters: [...commonHeaders],
|
|
56
77
|
requestBody: {
|
|
57
78
|
required: true,
|
|
58
79
|
content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } }
|
|
@@ -65,9 +86,10 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
65
86
|
tags: [capitalized],
|
|
66
87
|
summary: `Get all ${capitalized}s`,
|
|
67
88
|
parameters: [
|
|
68
|
-
|
|
69
|
-
{ name: '
|
|
70
|
-
{ name: '
|
|
89
|
+
...commonHeaders,
|
|
90
|
+
{ name: 'page', in: 'query', schema: { type: 'integer' }, description: 'Zero-based page index' },
|
|
91
|
+
{ name: 'size', in: 'query', schema: { type: 'integer' }, description: 'Records per page' },
|
|
92
|
+
{ name: 'eager', in: 'query', schema: { type: 'boolean' }, description: 'If true, performs SQL Join to fetch relations' }
|
|
71
93
|
],
|
|
72
94
|
responses: {
|
|
73
95
|
200: { description: 'OK', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
|
|
@@ -80,7 +102,11 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
80
102
|
get: {
|
|
81
103
|
tags: [capitalized],
|
|
82
104
|
summary: `Get ${capitalized} by ID`,
|
|
83
|
-
parameters: [
|
|
105
|
+
parameters: [
|
|
106
|
+
...commonHeaders,
|
|
107
|
+
{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
|
|
108
|
+
{ name: 'eager', in: 'query', schema: { type: 'boolean' } }
|
|
109
|
+
],
|
|
84
110
|
responses: {
|
|
85
111
|
200: { description: 'OK', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
|
|
86
112
|
}
|
|
@@ -88,7 +114,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
88
114
|
put: {
|
|
89
115
|
tags: [capitalized],
|
|
90
116
|
summary: `Update ${capitalized}`,
|
|
91
|
-
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
|
|
117
|
+
parameters: [...commonHeaders, { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
|
|
92
118
|
responses: {
|
|
93
119
|
200: { description: 'Updated', content: { 'application/json': { schema: { $ref: `#/components/schemas/${capitalized}` } } } }
|
|
94
120
|
}
|
|
@@ -96,32 +122,105 @@ export const generateSwaggerDocs = async (config, entities, outputDir) => {
|
|
|
96
122
|
delete: {
|
|
97
123
|
tags: [capitalized],
|
|
98
124
|
summary: `Delete ${capitalized}`,
|
|
99
|
-
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
|
|
125
|
+
parameters: [...commonHeaders, { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
|
|
100
126
|
responses: {
|
|
101
127
|
204: { description: 'No Content' }
|
|
102
128
|
}
|
|
103
129
|
}
|
|
104
130
|
};
|
|
131
|
+
|
|
132
|
+
// BULK Operations /entities/bulk
|
|
133
|
+
swagger.paths[`/${name}s/bulk`] = {
|
|
134
|
+
post: {
|
|
135
|
+
tags: [capitalized],
|
|
136
|
+
summary: `Bulk Create ${capitalized}s`,
|
|
137
|
+
parameters: [...commonHeaders],
|
|
138
|
+
requestBody: {
|
|
139
|
+
required: true,
|
|
140
|
+
content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } }
|
|
141
|
+
},
|
|
142
|
+
responses: {
|
|
143
|
+
201: { description: 'Created', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
put: {
|
|
147
|
+
tags: [capitalized],
|
|
148
|
+
summary: `Bulk Update ${capitalized}s`,
|
|
149
|
+
parameters: [...commonHeaders],
|
|
150
|
+
requestBody: {
|
|
151
|
+
required: true,
|
|
152
|
+
content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } }
|
|
153
|
+
},
|
|
154
|
+
responses: {
|
|
155
|
+
200: { description: 'Updated', content: { 'application/json': { schema: { type: 'array', items: { $ref: `#/components/schemas/${capitalized}` } } } } }
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
patch: {
|
|
159
|
+
tags: [capitalized],
|
|
160
|
+
summary: `Bulk Patch ${capitalized}s`,
|
|
161
|
+
parameters: [...commonHeaders],
|
|
162
|
+
requestBody: {
|
|
163
|
+
required: true,
|
|
164
|
+
content: {
|
|
165
|
+
'application/json': {
|
|
166
|
+
schema: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
items: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
id: { type: 'integer' },
|
|
172
|
+
changes: { type: 'object' }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
responses: {
|
|
180
|
+
200: { description: 'Patched' }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
105
184
|
}
|
|
106
185
|
|
|
107
186
|
// 2. Add System Paths
|
|
108
187
|
swagger.paths['/rpc/{table}'] = {
|
|
109
188
|
get: {
|
|
110
|
-
tags: ['Search'],
|
|
111
|
-
summary: 'Generic PostgREST
|
|
189
|
+
tags: ['Search Engine'],
|
|
190
|
+
summary: 'Generic PostgREST RPC Engine',
|
|
191
|
+
description: `Powerful dynamic querying system.
|
|
192
|
+
|
|
193
|
+
### Dynamic Filtering
|
|
194
|
+
Append any column name as a query parameter using operator notation:
|
|
195
|
+
- \`?age=gt.20\` (Greater Than)
|
|
196
|
+
- \`?name=ilike.John\` (Case-insensitive search)
|
|
197
|
+
- \`?id=in.1,2,3\` (Set containment)
|
|
198
|
+
|
|
199
|
+
### JSONB Path Querying
|
|
200
|
+
For JSON fields, use arrow notation:
|
|
201
|
+
- \`?metadata->>role=eq.ADMIN\` (Nested text extraction)
|
|
202
|
+
- \`?details->count=gt.5\` (Nested numeric extraction)`,
|
|
112
203
|
parameters: [
|
|
113
|
-
|
|
114
|
-
{ name: '
|
|
115
|
-
{ name: '
|
|
204
|
+
...commonHeaders,
|
|
205
|
+
{ name: 'table', in: 'path', required: true, schema: { type: 'string' }, description: 'The database table to query' },
|
|
206
|
+
{ name: 'order', in: 'query', schema: { type: 'string' }, description: 'Sorting (e.g., id.desc)' },
|
|
207
|
+
{ name: 'limit', in: 'query', schema: { type: 'integer' }, description: 'Row limit' },
|
|
208
|
+
{ name: 'offset', in: 'query', schema: { type: 'integer' }, description: 'Query offset' }
|
|
116
209
|
],
|
|
117
|
-
responses: {
|
|
210
|
+
responses: {
|
|
211
|
+
200: {
|
|
212
|
+
description: 'OK',
|
|
213
|
+
content: { 'application/json': { schema: { type: 'array', items: { type: 'object' } } } }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
118
216
|
}
|
|
119
217
|
};
|
|
120
218
|
|
|
121
219
|
swagger.paths['/audit'] = {
|
|
122
220
|
get: {
|
|
123
|
-
tags: ['
|
|
124
|
-
summary: '
|
|
221
|
+
tags: ['Observability'],
|
|
222
|
+
summary: 'Fetch Audit Trail',
|
|
223
|
+
parameters: [...commonHeaders],
|
|
125
224
|
responses: { 200: { description: 'OK' } }
|
|
126
225
|
}
|
|
127
226
|
};
|
|
@@ -139,7 +238,10 @@ const mapToSwaggerType = (type) => {
|
|
|
139
238
|
'Long': 'integer',
|
|
140
239
|
'BigDecimal': 'number',
|
|
141
240
|
'LocalDate': 'string',
|
|
142
|
-
'Instant': 'string'
|
|
241
|
+
'Instant': 'string',
|
|
242
|
+
'JSON': 'object',
|
|
243
|
+
'JSONB': 'object',
|
|
244
|
+
'Text': 'string'
|
|
143
245
|
};
|
|
144
246
|
return types[type] || 'string';
|
|
145
247
|
};
|
package/package.json
CHANGED
|
@@ -124,6 +124,84 @@
|
|
|
124
124
|
</div>
|
|
125
125
|
</div>
|
|
126
126
|
</div>
|
|
127
|
+
|
|
128
|
+
<!-- JSON Querying Section -->
|
|
129
|
+
<div class="bg-white border border-slate-200 rounded-2xl p-8 shadow-sm">
|
|
130
|
+
<h4 class="text-xl font-bold text-slate-900 mb-4 flex items-center">
|
|
131
|
+
<svg class="w-6 h-6 mr-3 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
|
132
|
+
Deep JSON/JSONB Querying
|
|
133
|
+
</h4>
|
|
134
|
+
<p class="text-slate-600 text-sm mb-6 leading-relaxed">
|
|
135
|
+
The GO-DUCK generator natively supports PostgreSQL JSONB operators. You can drill down into nested fields directly from the URL using arrow notation.
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
<div class="space-y-4">
|
|
139
|
+
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100">
|
|
140
|
+
<div class="flex items-center justify-between mb-2">
|
|
141
|
+
<span class="text-xs font-bold text-indigo-600 uppercase tracking-widest">Text Extraction (->>)</span>
|
|
142
|
+
<span class="px-2 py-0.5 rounded bg-indigo-100 text-indigo-700 text-[10px] font-bold">Standard Use</span>
|
|
143
|
+
</div>
|
|
144
|
+
<code class="text-xs text-slate-800">GET /api/rpc/users?metadata->>role=eq.ADMIN</code>
|
|
145
|
+
<p class="text-[11px] text-slate-500 mt-2">Extracts the value as text. Perfect for equality checks on nested strings.</p>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100">
|
|
149
|
+
<div class="flex items-center justify-between mb-2">
|
|
150
|
+
<span class="text-xs font-bold text-purple-600 uppercase tracking-widest">Object Extraction (->)</span>
|
|
151
|
+
</div>
|
|
152
|
+
<code class="text-xs text-slate-800">GET /api/rpc/orders?details->itemsCount=gt.5</code>
|
|
153
|
+
<p class="text-[11px] text-slate-500 mt-2">Treats the extracted value as a JSON object/numeric, allowing for range checks on nested numbers.</p>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="mt-6 p-4 bg-amber-50 rounded-xl border border-amber-100">
|
|
158
|
+
<p class="text-[11px] text-amber-800 leading-relaxed">
|
|
159
|
+
<strong>Pro Tip:</strong> For high-performance JSON querying, ensure you have a <code>GIN</code> index on the JSONB column in your database.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
<section class="mb-12">
|
|
166
|
+
<h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
|
|
167
|
+
<span class="w-8 h-8 rounded-lg bg-orange-100 text-orange-600 flex items-center justify-center mr-3 text-sm">
|
|
168
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
|
169
|
+
</span>
|
|
170
|
+
Bulk Mission Control (High Velocity)
|
|
171
|
+
</h2>
|
|
172
|
+
<p class="text-slate-600 text-sm mb-6 leading-relaxed">
|
|
173
|
+
For batch processing and migrations, avoid the overhead of multiple HTTP calls. Use the specialized <code class="bg-orange-50 px-1 rounded text-orange-700">/bulk</code> endpoints to process hundreds of records in a single transaction.
|
|
174
|
+
</p>
|
|
175
|
+
|
|
176
|
+
<div class="space-y-6">
|
|
177
|
+
<!-- Bulk Create -->
|
|
178
|
+
<div class="bg-white border border-slate-200 rounded-2xl overflow-hidden">
|
|
179
|
+
<div class="bg-slate-50 px-6 py-3 border-b border-slate-200 flex items-center justify-between">
|
|
180
|
+
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Bulk Create Transaction</span>
|
|
181
|
+
<span class="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 text-[10px] font-bold">POST /api/:entities/bulk</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="p-6">
|
|
184
|
+
<pre class="bg-slate-900 rounded-xl p-4 text-xs text-slate-300 font-mono">[
|
|
185
|
+
{ "title": "Bulk Article A", "status": "DRAFT" },
|
|
186
|
+
{ "title": "Bulk Article B", "status": "PUBLISHED" }
|
|
187
|
+
]</pre>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Bulk Patch -->
|
|
192
|
+
<div class="bg-white border border-slate-200 rounded-2xl overflow-hidden">
|
|
193
|
+
<div class="bg-slate-50 px-6 py-3 border-b border-slate-200 flex items-center justify-between">
|
|
194
|
+
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Multi-Entity Patch</span>
|
|
195
|
+
<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-700 text-[10px] font-bold">PATCH /api/:entities/bulk</span>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="p-6">
|
|
198
|
+
<pre class="bg-slate-900 rounded-xl p-4 text-xs text-slate-300 font-mono">[
|
|
199
|
+
{ "id": 1, "changes": { "status": "ARCHIVED" } },
|
|
200
|
+
{ "id": 2, "changes": { "title": "Updated Title via Bulk" } }
|
|
201
|
+
]</pre>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
127
205
|
</section>
|
|
128
206
|
|
|
129
207
|
<section class="mb-12">
|
|
@@ -205,6 +205,104 @@ return nil, nil
|
|
|
205
205
|
c.JSON(http.StatusOK, gin.H{"message": "Updated successfully", "data": entity})
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// BulkCreate handles creating multiple entities in one transaction
|
|
209
|
+
func (ctrl *{{capitalize name}}Controller) BulkCreate(c *gin.Context) {
|
|
210
|
+
tenant, _ := c.Get("tenantDB")
|
|
211
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
212
|
+
ctx := c.Request.Context()
|
|
213
|
+
|
|
214
|
+
var entities []models.{{capitalize name}}
|
|
215
|
+
if err := c.ShouldBindJSON(&entities); err != nil {
|
|
216
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if err := ctrl.DB.WithContext(ctx).Create(&entities).Error; err != nil {
|
|
221
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Dynamic Cache Invalidation (Tenant Aware)
|
|
226
|
+
cache.ClearPattern(tenantStr + ":{{capitalize name}}*")
|
|
227
|
+
|
|
228
|
+
// MQTT Event (Resilient)
|
|
229
|
+
resilience.Execute(func() (interface{}, error) {
|
|
230
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "BULK_CREATE", "{{capitalize name}}", entities, nil)
|
|
231
|
+
return nil, nil
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
c.JSON(http.StatusCreated, entities)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// BulkUpdate handles updating multiple entities in one transaction
|
|
238
|
+
func (ctrl *{{capitalize name}}Controller) BulkUpdate(c *gin.Context) {
|
|
239
|
+
tenant, _ := c.Get("tenantDB")
|
|
240
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
241
|
+
ctx := c.Request.Context()
|
|
242
|
+
|
|
243
|
+
var entities []models.{{capitalize name}}
|
|
244
|
+
if err := c.ShouldBindJSON(&entities); err != nil {
|
|
245
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
err := ctrl.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
250
|
+
for _, e := range entities {
|
|
251
|
+
if err := tx.Save(&e).Error; err != nil {
|
|
252
|
+
return err
|
|
253
|
+
}
|
|
254
|
+
cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%d", tenantStr, e.ID))
|
|
255
|
+
}
|
|
256
|
+
return nil
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if err != nil {
|
|
260
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// MQTT Event (Resilient)
|
|
265
|
+
resilience.Execute(func() (interface{}, error) {
|
|
266
|
+
messaging.PublishEvent(ctrl.Config.GoDuck.Messaging.MQTT.TopicPrefix, "BULK_UPDATE", "{{capitalize name}}", entities, nil)
|
|
267
|
+
return nil, nil
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
c.JSON(http.StatusOK, entities)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// BulkPatch handles partial updates for multiple entities
|
|
274
|
+
func (ctrl *{{capitalize name}}Controller) BulkPatch(c *gin.Context) {
|
|
275
|
+
tenant, _ := c.Get("tenantDB")
|
|
276
|
+
tenantStr := fmt.Sprintf("%v", tenant)
|
|
277
|
+
ctx := c.Request.Context()
|
|
278
|
+
|
|
279
|
+
var updates []struct {
|
|
280
|
+
ID uint `json:"id"`
|
|
281
|
+
Changes map[string]interface{} `json:"changes"`
|
|
282
|
+
}
|
|
283
|
+
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
284
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
err := ctrl.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
289
|
+
for _, u := range updates {
|
|
290
|
+
if err := tx.Model(&models.{{capitalize name}}{}).Where("id = ?", u.ID).Updates(u.Changes).Error; err != nil {
|
|
291
|
+
return err
|
|
292
|
+
}
|
|
293
|
+
cache.Delete(fmt.Sprintf("%s:{{capitalize name}}:%d", tenantStr, u.ID))
|
|
294
|
+
}
|
|
295
|
+
return nil
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
if err != nil {
|
|
299
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
c.JSON(http.StatusOK, gin.H{"message": "Bulk patch completed successfully"})
|
|
304
|
+
}
|
|
305
|
+
|
|
208
306
|
// Delete
|
|
209
307
|
func (ctrl *{{capitalize name}}Controller) Delete(c *gin.Context) {
|
|
210
308
|
id := c.Param("id")
|
package/templates/go/main.go.hbs
CHANGED
|
@@ -163,10 +163,13 @@ api.GET("/rpc/:table", searchCtrl.GenericSearch)
|
|
|
163
163
|
// {{name}} Routes
|
|
164
164
|
{{toLowerCase name}}Ctrl := controllers.{{capitalize name}}Controller{DB: masterDB, Config: appConfig}
|
|
165
165
|
api.POST("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.Create)
|
|
166
|
+
api.POST("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkCreate)
|
|
166
167
|
api.GET("/{{toLowerCase name}}s", {{toLowerCase name}}Ctrl.GetAll)
|
|
167
168
|
api.GET("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.GetByID)
|
|
168
169
|
api.PUT("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Update)
|
|
170
|
+
api.PUT("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkUpdate)
|
|
169
171
|
api.PATCH("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Patch)
|
|
172
|
+
api.PATCH("/{{toLowerCase name}}s/bulk", {{toLowerCase name}}Ctrl.BulkPatch)
|
|
170
173
|
api.DELETE("/{{toLowerCase name}}s/:id", {{toLowerCase name}}Ctrl.Delete)
|
|
171
174
|
{{/each}}
|
|
172
175
|
// go-duck-needle-add-route
|