go-duck-cli 1.2.1 → 1.2.3

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.
@@ -169,11 +169,25 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
169
169
  });
170
170
 
171
171
  // 3. GraphQL
172
- collection.item.push({
172
+ const generateGqlFieldsInput = (fields) => {
173
+ return fields.map(f => {
174
+ if (f.type === 'String' || f.type === 'Text') return `${f.name}: "Sample ${f.name}"`;
175
+ if (f.type === 'Integer' || f.type === 'Long') return `${f.name}: 100`;
176
+ if (f.type === 'Float' || f.type === 'BigDecimal') return `${f.name}: 99.99`;
177
+ if (f.type === 'Boolean') return `${f.name}: true`;
178
+ if (f.type === 'LocalDate') return `${f.name}: "2024-01-01"`;
179
+ if (f.type === 'Instant') return `${f.name}: "2024-01-01T12:00:00Z"`;
180
+ if (f.type === 'JSON' || f.type === 'JSONB') return `${f.name}: "{\\"attribute\\": \\"example_value\\"}"`;
181
+ return `${f.name}: "test"`;
182
+ }).join(',\n ');
183
+ };
184
+
185
+ const graphqlFolder = {
173
186
  name: "3. GraphQL Federation Layer",
187
+ description: "Federated GraphQL operations for all GDL entities.",
174
188
  item: [
175
189
  {
176
- name: "1. Unified GraphQL Engine",
190
+ name: "GraphQL Endpoint Playground",
177
191
  request: {
178
192
  method: "POST",
179
193
  header: [
@@ -184,7 +198,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
184
198
  body: {
185
199
  mode: "graphql",
186
200
  graphql: {
187
- query: `query {\n ${entities.length > 0 ? 'list' + entities[0].name.charAt(0).toUpperCase() + entities[0].name.slice(1) + 's' : 'test'} {\n id\n }\n}`,
201
+ query: "query {\n __schema {\n types {\n name\n }\n }\n}",
188
202
  variables: ""
189
203
  }
190
204
  },
@@ -198,7 +212,143 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
198
212
  }
199
213
  }
200
214
  ]
201
- });
215
+ };
216
+
217
+ for (const entity of entities) {
218
+ const capitalized = entity.name.charAt(0).toUpperCase() + entity.name.slice(1);
219
+ const entityGqlFolder = {
220
+ name: `${capitalized} GraphQL`,
221
+ item: [
222
+ {
223
+ name: `List ${capitalized}s (GraphQL Query)`,
224
+ request: {
225
+ method: "POST",
226
+ header: [
227
+ { key: "Authorization", value: "Bearer {{token}}" },
228
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
229
+ { key: "Content-Type", value: "application/json" }
230
+ ],
231
+ body: {
232
+ mode: "graphql",
233
+ graphql: {
234
+ query: `query {\n list${capitalized}s(page: 1, size: 10) {\n role\n items {\n id\n ${entity.fields.map(f => f.name).join('\n ')}\n createdAt\n updatedAt\n }\n }\n}`,
235
+ variables: ""
236
+ }
237
+ },
238
+ url: {
239
+ raw: "http://{{host}}:{{port}}/graphql",
240
+ protocol: "http",
241
+ host: ["{{host}}"],
242
+ port: "{{port}}",
243
+ path: ["graphql"]
244
+ }
245
+ }
246
+ },
247
+ {
248
+ name: `Get ${capitalized} by ID (GraphQL Query)`,
249
+ request: {
250
+ method: "POST",
251
+ header: [
252
+ { key: "Authorization", value: "Bearer {{token}}" },
253
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
254
+ { key: "Content-Type", value: "application/json" }
255
+ ],
256
+ body: {
257
+ mode: "graphql",
258
+ graphql: {
259
+ query: `query {\n get${capitalized}(id: "1") {\n role\n data {\n id\n ${entity.fields.map(f => f.name).join('\n ')}\n createdAt\n updatedAt\n }\n }\n}`,
260
+ variables: ""
261
+ }
262
+ },
263
+ url: {
264
+ raw: "http://{{host}}:{{port}}/graphql",
265
+ protocol: "http",
266
+ host: ["{{host}}"],
267
+ port: "{{port}}",
268
+ path: ["graphql"]
269
+ }
270
+ }
271
+ },
272
+ {
273
+ name: `Create ${capitalized} (GraphQL Mutation)`,
274
+ request: {
275
+ method: "POST",
276
+ header: [
277
+ { key: "Authorization", value: "Bearer {{token}}" },
278
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
279
+ { key: "Content-Type", value: "application/json" }
280
+ ],
281
+ body: {
282
+ mode: "graphql",
283
+ graphql: {
284
+ query: `mutation {\n create${capitalized}(input: {\n ${generateGqlFieldsInput(entity.fields)}\n }) {\n role\n data {\n id\n ${entity.fields.map(f => f.name).join('\n ')}\n createdAt\n updatedAt\n }\n }\n}`,
285
+ variables: ""
286
+ }
287
+ },
288
+ url: {
289
+ raw: "http://{{host}}:{{port}}/graphql",
290
+ protocol: "http",
291
+ host: ["{{host}}"],
292
+ port: "{{port}}",
293
+ path: ["graphql"]
294
+ }
295
+ }
296
+ },
297
+ {
298
+ name: `Update ${capitalized} (GraphQL Mutation)`,
299
+ request: {
300
+ method: "POST",
301
+ header: [
302
+ { key: "Authorization", value: "Bearer {{token}}" },
303
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
304
+ { key: "Content-Type", value: "application/json" }
305
+ ],
306
+ body: {
307
+ mode: "graphql",
308
+ graphql: {
309
+ query: `mutation {\n update${capitalized}(id: "1", input: {\n ${generateGqlFieldsInput(entity.fields)}\n }) {\n role\n data {\n id\n ${entity.fields.map(f => f.name).join('\n ')}\n createdAt\n updatedAt\n }\n }\n}`,
310
+ variables: ""
311
+ }
312
+ },
313
+ url: {
314
+ raw: "http://{{host}}:{{port}}/graphql",
315
+ protocol: "http",
316
+ host: ["{{host}}"],
317
+ port: "{{port}}",
318
+ path: ["graphql"]
319
+ }
320
+ }
321
+ },
322
+ {
323
+ name: `Delete ${capitalized} (GraphQL Mutation)`,
324
+ request: {
325
+ method: "POST",
326
+ header: [
327
+ { key: "Authorization", value: "Bearer {{token}}" },
328
+ { key: "X-Tenant-ID", value: "{{tenant}}" },
329
+ { key: "Content-Type", value: "application/json" }
330
+ ],
331
+ body: {
332
+ mode: "graphql",
333
+ graphql: {
334
+ query: `mutation {\n delete${capitalized}(id: "1")\n}`,
335
+ variables: ""
336
+ }
337
+ },
338
+ url: {
339
+ raw: "http://{{host}}:{{port}}/graphql",
340
+ protocol: "http",
341
+ host: ["{{host}}"],
342
+ port: "{{port}}",
343
+ path: ["graphql"]
344
+ }
345
+ }
346
+ }
347
+ ]
348
+ };
349
+ graphqlFolder.item.push(entityGqlFolder);
350
+ }
351
+ collection.item.push(graphqlFolder);
202
352
 
203
353
  // 4. REST Entities
204
354
  const restFolder = { name: "4. Standard REST & Deep Search", item: [] };
@@ -240,7 +390,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
240
390
  request: {
241
391
  method: "GET",
242
392
  header: [ { key: "X-Tenant-ID", value: "{{tenant}}" } ],
243
- url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" } ] }
393
+ url: { raw: `http://{{host}}:{{port}}/open/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["open", "api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
244
394
  }
245
395
  });
246
396
  publicItems.push({
@@ -275,7 +425,7 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
275
425
  request: {
276
426
  method: "GET",
277
427
  header: [ { key: "Authorization", value: "Bearer {{token}}" }, { key: "X-Tenant-ID", value: "{{tenant}}" } ],
278
- url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" } ] }
428
+ url: { raw: `http://{{host}}:{{port}}/api/${name}s?page=1&size=10&eager=true&sort=id,asc`, protocol: "http", host: ["{{host}}"], port: "{{port}}", path: ["api", `${name}s`], query: [ { key: "page", value: "1" }, { key: "size", value: "10" }, { key: "eager", value: "true" }, { key: "sort", value: "id,asc" } ] }
279
429
  }
280
430
  },
281
431
  {
@@ -21,7 +21,7 @@ import (
21
21
  "time"
22
22
 
23
23
  "github.com/gin-gonic/gin"
24
- "github.com/golang-jwt/jwt/v4"
24
+ "github.com/golang-jwt/jwt/v5"
25
25
  "{{app_name}}/config"
26
26
  )
27
27
 
@@ -164,7 +164,7 @@ func JWTMiddleware() gin.HandlerFunc {
164
164
  return nil, fmt.Errorf("missing kid header")
165
165
  }
166
166
  return GetPublicKeyByKid(kid, config.GetConfig())
167
- })
167
+ }, jwt.WithLeeway(10*time.Second))
168
168
 
169
169
  if err != nil || !token.Valid {
170
170
  c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
package/index.js CHANGED
@@ -357,7 +357,7 @@ const getPreviousEntities = async (outputDir) => {
357
357
  return entities;
358
358
  };
359
359
 
360
- const generateEntities = async (gdlPath, outputDir, config) => {
360
+ const generateEntities = async (gdlPath, outputDir, config, isImport = false) => {
361
361
  let entities = [];
362
362
  let relationships = [];
363
363
  let enums = [];
@@ -399,6 +399,47 @@ const generateEntities = async (gdlPath, outputDir, config) => {
399
399
  console.log(chalk.green(`✅ Parsed ${entities.length} entities, ${relationships.length} relationships, ${enums.length} enums, and detected ${openEntities.length} open rules`));
400
400
 
401
401
  const previousEntities = await getPreviousEntities(outputDir);
402
+
403
+ // Any parsed entity with @Delete is mapped as a deletion
404
+ const deletedParsed = entities.filter(e => e.isDelete);
405
+
406
+ // Remove deleted entities from active entities array
407
+ entities = entities.filter(e => !e.isDelete);
408
+
409
+ // Filter out relationships & open rule entries that involve deleted entities
410
+ relationships = relationships.filter(rel =>
411
+ !deletedParsed.some(d => d.name === rel.from.entity || d.name === rel.to.entity)
412
+ );
413
+ openEntities = openEntities.filter(oe =>
414
+ !deletedParsed.some(d => d.name === oe.name)
415
+ );
416
+
417
+ if (isImport) {
418
+ for (const prev of previousEntities) {
419
+ if (!entities.some(e => e.name === prev.name) && !deletedParsed.some(d => d.name === prev.name)) {
420
+ entities.push(prev);
421
+ if (prev.relationships) {
422
+ for (const r of prev.relationships) {
423
+ const exists = relationships.some(rel =>
424
+ (rel.from.entity === r.from.entity && rel.from.field === r.from.field && rel.to.entity === r.to.entity && rel.to.field === r.to.field) ||
425
+ (rel.from.entity === r.to.entity && rel.from.field === r.to.field && rel.to.entity === r.from.entity && rel.to.field === r.from.field)
426
+ );
427
+ if (!exists) relationships.push(r);
428
+ }
429
+ }
430
+ if (prev.enums) {
431
+ for (const en of prev.enums) {
432
+ if (!enums.some(e => e.name === en.name)) enums.push(en);
433
+ }
434
+ }
435
+ if (prev.openEntities) {
436
+ for (const oe of prev.openEntities) {
437
+ if (!openEntities.some(o => o.name === oe.name)) openEntities.push(oe);
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
402
443
  const delta = {
403
444
  newEntities: [],
404
445
  newFields: {},
@@ -443,9 +484,18 @@ const generateEntities = async (gdlPath, outputDir, config) => {
443
484
  }
444
485
 
445
486
  // Check for deleted entities
446
- for (const prev of previousEntities) {
447
- if (!entities.some(e => e.name === prev.name)) {
448
- delta.deletedEntities.push(prev);
487
+ if (isImport) {
488
+ for (const d of deletedParsed) {
489
+ const prev = previousEntities.find(e => e.name === d.name);
490
+ if (prev) {
491
+ delta.deletedEntities.push(prev);
492
+ }
493
+ }
494
+ } else {
495
+ for (const prev of previousEntities) {
496
+ if (!entities.some(e => e.name === prev.name)) {
497
+ delta.deletedEntities.push(prev);
498
+ }
449
499
  }
450
500
  }
451
501
 
@@ -762,7 +812,7 @@ require (
762
812
  await generateResilienceCode(config, absoluteOutputDir);
763
813
  await generateTelemetryCode(config, absoluteOutputDir);
764
814
  await generateDeploymentArtifacts(config, absoluteOutputDir);
765
- const { entities, relationships, enums, openEntities } = await generateEntities(gdlPath, absoluteOutputDir, config);
815
+ const { entities, relationships, enums, openEntities } = await generateEntities(gdlPath, absoluteOutputDir, config, true);
766
816
  await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
767
817
 
768
818
  await generateRepositoryCode(absoluteOutputDir);
@@ -828,13 +878,18 @@ const generateYAMLConfigs = async (config, outputDir) => {
828
878
  const extendedConfig = {
829
879
  ...cleanConfig,
830
880
  server: {
831
- port: cleanConfig.server?.port || 8080,
881
+ rest: {
882
+ port: cleanConfig.server?.rest?.port || cleanConfig.server?.port || 8080,
883
+ protocol: cleanConfig.server?.rest?.protocol || 'json'
884
+ },
832
885
  'read-timeout': cleanConfig.server?.['read-timeout'] || '30s',
833
886
  'write-timeout': cleanConfig.server?.['write-timeout'] || '30s',
834
887
  grpc: {
835
888
  addr: cleanConfig.server?.grpc?.addr || ':9000',
836
889
  network: cleanConfig.server?.grpc?.network || 'tcp',
837
- timeout: cleanConfig.server?.grpc?.timeout || '1s'
890
+ timeout: cleanConfig.server?.grpc?.timeout || '1s',
891
+ web_enabled: cleanConfig.server?.grpc?.web_enabled ?? true,
892
+ web_port: cleanConfig.server?.grpc?.web_port || 9090
838
893
  },
839
894
  cors: {
840
895
  'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['*'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/parser/gdl.js CHANGED
@@ -179,6 +179,7 @@ export const parseGDL = async (filePath) => {
179
179
  const isSearchable = annotation?.includes('@Searchable');
180
180
  const isDocument = annotation?.includes('@Document') || annotation?.includes('@isDocument');
181
181
  const isEmbedded = annotation?.includes('@Embed');
182
+ const isDelete = annotation?.includes('@Delete');
182
183
 
183
184
  const fields = parseFields(block.fieldBlock);
184
185
 
@@ -190,6 +191,7 @@ export const parseGDL = async (filePath) => {
190
191
  isSearchable,
191
192
  isDocument,
192
193
  isEmbedded,
194
+ isDelete,
193
195
  fields
194
196
  });
195
197
  }
@@ -69,7 +69,8 @@ go run main.go</code></pre>
69
69
  <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">1. REST APIs & Generic Search</h2>
70
70
  <p class="mb-4">The application provides standard RESTful CRUD endpoints for all generated entities (e.g., {{#if entities.length}}{{entities.[0].name}}{{else}}Entity{{/if}}).</p>
71
71
 
72
- <h3 class="font-semibold mb-2">Standard CRUD:</h3>
72
+ <h3 class="font-semibold mb-2">Standard CRUD (Multi-Protocol):</h3>
73
+ <p class="mb-2 text-sm text-gray-600">The REST API natively supports JSON and MessagePack based on your <code>application.yml</code> settings.</p>
73
74
  <pre><code class="language-http">GET /api/{{#if entities.length}}{{toLowerCase entities.[0].name}}s{{else}}entities{{/if}}?page=1&pageSize=10
74
75
  POST /api/{{#if entities.length}}{{toLowerCase entities.[0].name}}s{{else}}entities{{/if}}
75
76
  PUT /api/{{#if entities.length}}{{toLowerCase entities.[0].name}}s{{else}}entities{{/if}}/:id
@@ -81,6 +82,17 @@ DELETE /api/{{#if entities.length}}{{toLowerCase entities.[0].name}}s{{else}}ent
81
82
  -H "Authorization: Bearer YOUR_JWT" \
82
83
  -H "X-Tenant-ID: tenant_1"</code></pre>
83
84
  <p class="text-sm text-gray-500 mt-2">Supported operators: <code>eq, neq, gt, gte, lt, lte, like, ilike</code></p>
85
+
86
+ <h3 class="font-semibold mb-2 mt-6">Elasticsearch Global Search (Spring-style):</h3>
87
+ <p class="mb-2">For entities marked with <code>@Searchable</code>, use the native Elasticsearch endpoint for advanced queries (wildcards, booleans, and ranges).</p>
88
+ <pre><code class="language-bash">curl -G "http://localhost:{{serverPort}}/api/search/{{#if entities.length}}{{toLowerCase entities.[0].name}}{{else}}entity{{/if}}" \
89
+ --data-urlencode "q=name:John AND age:>18" \
90
+ -H "Authorization: Bearer YOUR_JWT"</code></pre>
91
+ <ul class="text-sm text-gray-500 mt-2 list-disc pl-5">
92
+ <li><strong>Wildcards:</strong> <code>q=name*</code> (matches "name15", "name_abc")</li>
93
+ <li><strong>Boolean Logic:</strong> <code>q=status:PUBLISHED AND (author:John OR author:Jane)</code></li>
94
+ <li><strong>Ranges:</strong> <code>q=age:[18 TO 30]</code> or <code>q=created_at:>2023-01-01</code></li>
95
+ </ul>
84
96
  </section>
85
97
 
86
98
  <!-- Audit & Metering -->
@@ -127,6 +139,17 @@ mosquitto_sub -h localhost -p {{mqttPort}} -t "go-duck/events/#" -u dev_user -P
127
139
  <p class="text-sm mt-2">Example topic: <code>go-duck/events/{{#if entities.length}}{{toLowerCase entities.[0].name}}{{else}}entity{{/if}}/CREATE</code></p>
128
140
  </section>
129
141
 
142
+ <!-- gRPC-Web -->
143
+ <section id="grpc-web" class="content-section">
144
+ <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">gRPC & gRPC-Web</h2>
145
+ <p class="mb-4">The Kratos gRPC engine powers blazing fast service-to-service communication. For frontend developers, we automatically start a <strong>gRPC-Web Proxy</strong>.</p>
146
+ <ul class="list-disc pl-6 space-y-2 mb-4">
147
+ <li>The core Kratos gRPC server runs on <code>:9000</code> (configurable).</li>
148
+ <li>The <strong>gRPC-Web Proxy</strong> runs on <code>:9090</code>, translating HTTP/1.1 calls to HTTP/2.</li>
149
+ <li>Frontend apps (React/Angular) can directly call Protobuf endpoints using standard <code>grpc-web</code> generated clients without hitting the REST APIs!</li>
150
+ </ul>
151
+ </section>
152
+
130
153
  <!-- Kratos gRPC -->
131
154
  <section id="grpc-kratos" class="content-section">
132
155
  <h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">Kratos Secured gRPC APIs</h2>
@@ -44,17 +44,33 @@ go-duck import-gdl my_new_schema.gdl -o ./MY_APP</code></pre>
44
44
  <span class="w-8 h-8 rounded-lg bg-emerald-100 text-emerald-600 flex items-center justify-center mr-3 text-sm">
45
45
  <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="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg>
46
46
  </span>
47
- 2. Persistence Intelligence & the `.go-duck/` Folder
47
+ 2. Schema Evolution & the `.go-duck/` State
48
48
  </h2>
49
- <p class="mb-4 text-slate-600 leading-relaxed font-medium">"If I run the generator twice, will it overwrite my database migrations?" <strong>No.</strong></p>
49
+ <p class="mb-4 text-slate-600 leading-relaxed font-medium">"If I run the generator to import a new GDL, will it overwrite my database or delete existing entities?" <strong>No, thanks to stateful evolution.</strong></p>
50
50
 
51
- <p class="mb-6 text-slate-600 leading-relaxed">The generator maintains a stateful snapshot of every entity it has ever generated inside the hidden <code class="bg-slate-100 px-1 py-0.5 rounded text-slate-800 font-mono text-sm">.go-duck/</code> directory at the root of your target project. When you run <code>import-gdl</code>, the code parser literally diffs your new file against the JSON representations in <code>.go-duck/</code> to intelligently assess the exact Table changes necessary without executing ghost migrations.</p>
51
+ <p class="mb-6 text-slate-600 leading-relaxed">The generator maintains a stateful snapshot of every entity it has ever generated inside the hidden <code class="bg-slate-100 px-1 py-0.5 rounded text-slate-800 font-mono text-sm">.go-duck/</code> directory at the root of your target project. When you run <code>import-gdl</code>, the CLI intelligently merges existing entities with newly parsed entities and executes targeted diff operations:</p>
52
+
53
+ <!-- EVOLUTION TYPE CARDS -->
54
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
55
+ <div class="p-6 bg-slate-50 border border-slate-200 rounded-2xl">
56
+ <span class="text-xs font-bold text-indigo-600 uppercase tracking-wide block mb-2 font-mono">1. Snapshot Merging</span>
57
+ <p class="text-xs text-slate-600 leading-relaxed m-0">Supports splitting your entities into multiple GDL files. Unspecified active models are loaded from the <code>.go-duck/</code> folder and merged with new entities, keeping active routers and endpoints in sync.</p>
58
+ </div>
59
+ <div class="p-6 bg-slate-50 border border-slate-200 rounded-2xl">
60
+ <span class="text-xs font-bold text-emerald-600 uppercase tracking-wide block mb-2 font-mono">2. Column Alterations</span>
61
+ <p class="text-xs text-slate-600 leading-relaxed m-0">Adding, dropping, or modifying fields inside entity blocks generates specific <code>ADD COLUMN</code> or <code>DROP COLUMN</code> SQL statements in a timestamped Goose migration file.</p>
62
+ </div>
63
+ <div class="p-6 bg-slate-50 border border-slate-200 rounded-2xl">
64
+ <span class="text-xs font-bold text-rose-600 uppercase tracking-wide block mb-2 font-mono">3. Complete Purging</span>
65
+ <p class="text-xs text-slate-600 leading-relaxed m-0">Marking an entity with the <code>@Delete</code> annotation automatically triggers a database <code>DROP TABLE</code> SQL migration, purges all generated Go/Protobuf code files, and clears its snapshot.</p>
66
+ </div>
67
+ </div>
52
68
 
53
69
  <div class="bg-rose-50 border border-rose-200 p-5 mb-6 rounded-xl flex items-start shadow-sm shadow-rose-100/50">
54
70
  <svg class="w-6 h-6 text-rose-500 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
55
71
  <div>
56
72
  <h4 class="font-bold text-rose-900 mb-1">Warning</h4>
57
- <p class="text-rose-800 text-sm leading-relaxed"><strong>Never delete `.go-duck/`</strong> unless you are intentionally wiping the database and starting configuration completely from zero.</p>
73
+ <p class="text-rose-800 text-sm leading-relaxed"><strong>Never manually delete `.go-duck/`</strong> unless you are intentionally wiping the entire database state and starting configuration completely from scratch.</p>
58
74
  </div>
59
75
  </div>
60
76
  </section>
@@ -100,6 +100,23 @@
100
100
  <p class="text-[11px] text-indigo-900 m-0 font-medium font-mono leading-tight">Architectural Impact: Scaffolds routes into the <code class="text-indigo-600 text-[10px]">/api/open/*</code> group, bypassing the OIDC Validator middleware chain.</p>
101
101
  </div>
102
102
  </div>
103
+
104
+ <!-- @Delete -->
105
+ <div class="p-10 bg-rose-50 border border-rose-100 rounded-[3rem] shadow-sm transition-all duration-300 hover:shadow-rose-200 group relative overflow-hidden">
106
+ <div class="absolute top-0 right-0 p-6 opacity-[0.03] group-hover:opacity-[0.07] transition-opacity">
107
+ <svg class="w-32 h-32 text-rose-900" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
108
+ </div>
109
+ <div class="flex items-center justify-between mb-8">
110
+ <code class="text-rose-700 font-mono font-black text-2xl group-hover:scale-105 transition-transform">@Delete</code>
111
+ <div class="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-2xl shadow-sm">🗑️</div>
112
+ </div>
113
+ <h4 class="text-lg font-black text-slate-900 mb-4 m-0 uppercase tracking-tighter">Entity Deletion</h4>
114
+ <p class="text-sm text-slate-700 leading-relaxed m-0 italic mb-6">Triggers full entity cleanup. Upon import, the generator purges all generated source code (models, controllers, repositories) and generates a database <code>DROP TABLE</code> migration.</p>
115
+ <div class="p-4 bg-white/50 rounded-2xl border border-rose-200 border-dashed">
116
+ <span class="text-[10px] font-bold text-rose-400 uppercase tracking-widest block mb-2 font-mono">Architectural Impact</span>
117
+ <p class="text-[11px] text-rose-900 m-0 font-medium font-mono leading-tight">Removes snapshots from .go-duck/, wipes Go files, and scaffolds drop SQL statements.</p>
118
+ </div>
119
+ </div>
103
120
  </div>
104
121
 
105
122
  <!-- COMPLEX EXAMPLE -->
@@ -129,6 +129,27 @@
129
129
  </div>
130
130
  </section>
131
131
 
132
+ <!-- PAGINATION & SORTING -->
133
+ <section class="mb-20">
134
+ <h2 class="text-3xl font-black text-slate-900 mb-8 tracking-tight italic underline decoration-indigo-600 underline-offset-8">Pagination & Dynamic Sorting</h2>
135
+ <div class="p-8 bg-slate-50 border border-slate-200 rounded-[2.5rem] shadow-sm">
136
+ <p class="text-slate-600 mb-6">List endpoints support pagination and dynamic sorting for both relational (PostgreSQL) and document-based (MongoDB) silos.</p>
137
+
138
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
139
+ <div class="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm border-l-4 border-l-indigo-600">
140
+ <h4 class="font-bold text-slate-900 mb-2">Pagination</h4>
141
+ <p class="text-xs text-slate-500 mb-4">Specify the page number (1-indexed) and limit size via standard query parameters. The response contains total count in the <code>X-Total-Count</code> header.</p>
142
+ <code class="text-xs font-mono font-bold text-indigo-700 bg-indigo-50 px-3 py-1 rounded-full">GET /api/v1/car?page=1&amp;size=20</code>
143
+ </div>
144
+ <div class="p-6 bg-white rounded-2xl border border-slate-100 shadow-sm border-l-4 border-l-emerald-600">
145
+ <h4 class="font-bold text-slate-900 mb-2">Dynamic Sorting</h4>
146
+ <p class="text-xs text-slate-500 mb-4">Sort fields dynamically in ascending (default or <code>asc</code>) or descending (<code>desc</code>) order. The sorting format is <code>?sort=fieldname,direction</code>.</p>
147
+ <code class="text-xs font-mono font-bold text-emerald-700 bg-emerald-50 px-3 py-1 rounded-full">GET /api/v1/car?sort=price,desc</code>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </section>
152
+
132
153
  <!-- RPC OPERATORS -->
133
154
  <section class="mb-10 text-center">
134
155
  <h2 class="text-2xl font-black text-slate-900 mb-6 italic italic underline decoration-indigo-200 underline-offset-8 decoration-8 font-serif uppercase tracking-widest">GORM Filter Reference</h2>