go-duck-cli 1.0.9 → 1.1.12

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 (70) hide show
  1. package/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +493 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
@@ -5,236 +5,130 @@ import { toLiquibaseType } from '../parser/gdl.js';
5
5
 
6
6
  export const generateLiquibaseChangelogs = async (entities, relationships, projectRootDir, delta = null, enums = []) => {
7
7
  const migrationsDir = path.join(projectRootDir, 'migrations');
8
- const liquibaseDir = path.join(migrationsDir, 'liquibase');
9
- const changelogsDir = path.join(liquibaseDir, 'changelogs');
8
+ const sqlDir = path.join(migrationsDir, 'sql');
10
9
  await fs.ensureDir(migrationsDir);
11
- await fs.ensureDir(liquibaseDir);
12
- await fs.ensureDir(changelogsDir);
10
+ await fs.ensureDir(sqlDir);
13
11
 
14
12
  const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
15
- const dateStamp = timestamp.substring(0, 8); // YYYYMMDD
16
13
 
17
14
  // -------------------------------------------------------
18
- // STEP 1: Build changesets FIRST decide on filename after
15
+ // Core Management Tables (Always present if they don't exist)
19
16
  // -------------------------------------------------------
20
- let changeSets = '';
17
+ if (!delta) {
18
+ const coreFileName = `00000_init_core_tables.sql`;
19
+ const corePath = path.join(sqlDir, coreFileName);
20
+
21
+ if (!await fs.pathExists(corePath)) {
22
+ const coreSql = `-- +goose Up
23
+ -- +goose StatementBegin
24
+ CREATE TABLE IF NOT EXISTS tenant_roles (
25
+ id BIGSERIAL PRIMARY KEY,
26
+ role_name VARCHAR(255) UNIQUE NOT NULL,
27
+ db_name VARCHAR(255) NOT NULL
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS audit_log (
31
+ id BIGSERIAL PRIMARY KEY,
32
+ entity_name VARCHAR(255) NOT NULL,
33
+ tenant_db VARCHAR(255),
34
+ entity_id VARCHAR(255) NOT NULL,
35
+ action VARCHAR(50) NOT NULL,
36
+ previous_value TEXT,
37
+ new_value TEXT,
38
+ modified_by VARCHAR(255),
39
+ keycloak_id VARCHAR(255),
40
+ modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+ client_ip VARCHAR(50)
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS api_usage (
45
+ id BIGSERIAL PRIMARY KEY,
46
+ user_id VARCHAR(255) NOT NULL,
47
+ api_path VARCHAR(255) NOT NULL,
48
+ tenant_db VARCHAR(255),
49
+ usage_count BIGINT NOT NULL DEFAULT 0,
50
+ max_limit BIGINT NOT NULL DEFAULT 1000,
51
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP
52
+ );
53
+ -- +goose StatementEnd
54
+
55
+ -- +goose Down
56
+ -- +goose StatementBegin
57
+ DROP TABLE IF EXISTS tenant_roles CASCADE;
58
+ DROP TABLE IF EXISTS audit_log CASCADE;
59
+ DROP TABLE IF EXISTS api_usage CASCADE;
60
+ -- +goose StatementEnd
61
+ `;
62
+ await fs.writeFile(corePath, coreSql);
63
+ }
64
+ }
65
+
66
+ // -------------------------------------------------------
67
+ // Entity Migrations
68
+ // -------------------------------------------------------
69
+ let sqlUp = '-- +goose Up\n-- +goose StatementBegin\n';
70
+ let sqlDown = '-- +goose Down\n-- +goose StatementBegin\n';
21
71
 
22
72
  const entitiesToCreate = delta ? delta.newEntities : entities;
23
- const relationshipsToAdd = delta ? delta.newRelationships : relationships;
73
+
74
+ if (entitiesToCreate.length === 0 && (!delta || (!delta.newFields && !delta.newRelationships))) {
75
+ return;
76
+ }
24
77
 
25
78
  // Create New Tables
26
79
  for (const entity of entitiesToCreate) {
27
- let columns = `
28
- <column name="id" type="BIGINT" autoIncrement="true">
29
- <constraints primaryKey="true" nullable="false"/>
30
- </column>`;
80
+ let columns = ' id BIGSERIAL PRIMARY KEY';
31
81
 
32
82
  for (const field of entity.fields) {
33
- const liqType = toLiquibaseType(field, enums);
34
- const uniqueConstraint = field.unique ? ' <constraints nullable="' + (field.required ? 'false' : 'true') + '" unique="true"/>' : '';
35
- if (field.unique) {
36
- columns += `
37
- <column name="${field.name.toLowerCase()}" type="${liqType}">
38
- <constraints nullable="${field.required ? 'false' : 'true'}" unique="true"/>
39
- </column>`;
40
- } else {
41
- columns += `
42
- <column name="${field.name.toLowerCase()}" type="${liqType}">
43
- <constraints nullable="${field.required ? 'false' : 'true'}"/>
44
- </column>`;
45
- }
83
+ let sqlType = toLiquibaseType(field, enums);
84
+ if (sqlType === 'JSON') sqlType = 'JSONB'; // Force JSONB for Postgres
85
+
86
+ const constraints = [];
87
+ if (field.required) constraints.push('NOT NULL');
88
+ if (field.unique) constraints.push('UNIQUE');
89
+
90
+ columns += `,\n ${field.name.toLowerCase()} ${sqlType} ${constraints.join(' ')}`;
46
91
  }
47
92
 
48
- // @Audited entities get full audit columns (skip the simpler created_at/updated_at to avoid duplicates)
93
+ // Auditing / Timestamp columns
49
94
  if (entity.annotation === '@Audited') {
50
- columns += `
51
- <column name="created_by" type="VARCHAR(255)"/>
52
- <column name="created_date" type="TIMESTAMP"/>
53
- <column name="last_modified_by" type="VARCHAR(255)"/>
54
- <column name="last_modified_date" type="TIMESTAMP"/>
55
- <column name="last_modified_user_id" type="VARCHAR(255)"/>`;
95
+ columns += `,\n created_by VARCHAR(255),\n created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_modified_by VARCHAR(255),\n last_modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_modified_user_id VARCHAR(255)`;
56
96
  } else {
57
- columns += `
58
- <column name="created_at" type="TIMESTAMP"/>
59
- <column name="updated_at" type="TIMESTAMP"/>`;
97
+ columns += `,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`;
60
98
  }
61
99
 
62
- changeSets += `
63
- <changeSet id="${entity.name.toLowerCase()}-create-${timestamp}" author="go-duck">
64
- <preConditions onFail="MARK_RAN"><not><tableExists tableName="${entity.name.toLowerCase()}"/></not></preConditions>
65
- <createTable tableName="${entity.name.toLowerCase()}">
66
- ${columns}
67
- </createTable>
68
- </changeSet>
69
- `;
100
+ sqlUp += `CREATE TABLE IF NOT EXISTS ${entity.name.toLowerCase()} (\n${columns}\n);\n\n`;
101
+ sqlDown += `DROP TABLE IF EXISTS ${entity.name.toLowerCase()} CASCADE;\n`;
70
102
  }
71
103
 
72
- // Add New Fields to existing tables
104
+ // Add New Fields
73
105
  if (delta && delta.newFields) {
74
106
  for (const [entityName, fields] of Object.entries(delta.newFields)) {
75
- if (fields.length === 0) continue;
76
- let columnTags = '';
77
107
  for (const field of fields) {
78
- const liqType = toLiquibaseType(field, enums);
79
- if (field.unique) {
80
- columnTags += `
81
- <column name="${field.name.toLowerCase()}" type="${liqType}">
82
- <constraints nullable="${field.required ? 'false' : 'true'}" unique="true"/>
83
- </column>`;
84
- } else {
85
- columnTags += `
86
- <column name="${field.name.toLowerCase()}" type="${liqType}">
87
- <constraints nullable="${field.required ? 'false' : 'true'}"/>
88
- </column>`;
89
- }
108
+ let sqlType = toLiquibaseType(field, enums);
109
+ if (sqlType === 'JSON') sqlType = 'JSONB';
110
+ const constraints = [];
111
+ if (field.required) constraints.push('NOT NULL');
112
+ if (field.unique) constraints.push('UNIQUE');
113
+
114
+ sqlUp += `ALTER TABLE ${entityName.toLowerCase()} ADD COLUMN IF NOT EXISTS ${field.name.toLowerCase()} ${sqlType} ${constraints.join(' ')};\n`;
115
+ sqlDown += `ALTER TABLE ${entityName.toLowerCase()} DROP COLUMN IF EXISTS ${field.name.toLowerCase()};\n`;
90
116
  }
91
- changeSets += `
92
- <changeSet id="${entityName.toLowerCase()}-add-fields-${timestamp}" author="go-duck">
93
- <addColumn tableName="${entityName.toLowerCase()}">
94
- ${columnTags}
95
- </addColumn>
96
- </changeSet>
97
- `;
98
- }
99
- }
100
-
101
- // Foreign Keys + Index
102
- for (const rel of relationshipsToAdd) {
103
- if (rel.type === 'OneToMany') {
104
- const childTable = rel.to.entity.toLowerCase();
105
- const fkNullable = rel.required ? 'false' : 'true';
106
- const fkCol = rel.to.field.toLowerCase() + '_id';
107
- const parentTable = rel.from.entity.toLowerCase();
108
- const fkName = `fk_${childTable}_${rel.to.field.toLowerCase()}`;
109
- const idxName = `idx_${childTable}_${fkCol}`;
110
- changeSets += `
111
- <changeSet id="rel-${parentTable}-${childTable}-${timestamp}" author="go-duck">
112
- <preConditions onFail="MARK_RAN">
113
- <not>
114
- <columnExists tableName="${childTable}" columnName="${fkCol}"/>
115
- </not>
116
- </preConditions>
117
- <addColumn tableName="${childTable}">
118
- <column name="${fkCol}" type="BIGINT">
119
- <constraints nullable="${fkNullable}" foreignKeyName="${fkName}" referencedTableName="${parentTable}" referencedColumnNames="id"/>
120
- </column>
121
- </addColumn>
122
- </changeSet>
123
-
124
- <changeSet id="idx-${idxName}-${timestamp}" author="go-duck">
125
- <preConditions onFail="MARK_RAN">
126
- <not><indexExists indexName="${idxName}"/></not>
127
- </preConditions>
128
- <createIndex indexName="${idxName}" tableName="${childTable}">
129
- <column name="${fkCol}"/>
130
- </createIndex>
131
- </changeSet>
132
- `;
133
117
  }
134
118
  }
135
119
 
136
- // Support Tables (always on initial run, never on incremental)
137
- if (!delta) {
138
- changeSets += `
139
- <changeSet id="management-tables-init" author="go-duck">
140
- <preConditions onFail="MARK_RAN"><not><tableExists tableName="tenant_roles"/></not></preConditions>
141
- <createTable tableName="tenant_roles">
142
- <column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
143
- <column name="role_name" type="VARCHAR(255)"><constraints nullable="false" unique="true"/></column>
144
- <column name="db_name" type="VARCHAR(255)"><constraints nullable="false"/></column>
145
- </createTable>
146
- </changeSet>
147
-
148
- <changeSet id="audit-log-table-init" author="go-duck">
149
- <preConditions onFail="MARK_RAN"><not><tableExists tableName="audit_log"/></not></preConditions>
150
- <createTable tableName="audit_log">
151
- <column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
152
- <column name="entity_name" type="VARCHAR(255)"><constraints nullable="false"/></column>
153
- <column name="entity_id" type="VARCHAR(255)"><constraints nullable="false"/></column>
154
- <column name="action" type="VARCHAR(50)"><constraints nullable="false"/></column>
155
- <column name="previous_value" type="TEXT"/><column name="new_value" type="TEXT"/>
156
- <column name="modified_by" type="VARCHAR(255)"/><column name="keycloak_id" type="VARCHAR(255)"/>
157
- <column name="modified_at" type="TIMESTAMP"><constraints nullable="false"/></column>
158
- <column name="client_ip" type="VARCHAR(50)"/>
159
- </createTable>
160
- </changeSet>
161
-
162
- <changeSet id="api-usage-table-init" author="go-duck">
163
- <preConditions onFail="MARK_RAN"><not><tableExists tableName="api_usage"/></not></preConditions>
164
- <createTable tableName="api_usage">
165
- <column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
166
- <column name="user_id" type="VARCHAR(255)"><constraints nullable="false"/></column>
167
- <column name="api_path" type="VARCHAR(255)"><constraints nullable="false"/></column>
168
- <column name="usage_count" type="BIGINT" defaultValueNumeric="0"><constraints nullable="false"/></column>
169
- <column name="max_limit" type="BIGINT" defaultValueNumeric="1000"><constraints nullable="false"/></column>
170
- <column name="last_accessed" type="TIMESTAMP"/>
171
- </createTable>
172
- </changeSet>
173
- `;
174
- }
175
-
176
- // -------------------------------------------------------
177
- // STEP 2: Only write files if there are real changesets
178
- // -------------------------------------------------------
179
- if (changeSets.trim() === '') {
180
- console.log(chalk.gray(' No database changes detected.'));
181
- return;
182
- }
120
+ sqlUp += '-- +goose StatementEnd\n\n';
121
+ sqlDown += '-- +goose StatementEnd\n';
183
122
 
184
- // Build descriptive filename from what actually changed
185
123
  const descParts = [];
186
124
  if (!delta) {
187
- const entityNames = entities.map(e => e.name.toLowerCase()).join('-');
188
- descParts.push('init');
189
- if (entityNames) descParts.push(entityNames);
190
- } else {
191
- if (delta.newEntities && delta.newEntities.length > 0)
192
- descParts.push('create-' + delta.newEntities.map(e => e.name.toLowerCase()).join('-'));
193
- if (delta.newFields && Object.keys(delta.newFields).length > 0)
194
- descParts.push('add-fields-to-' + Object.keys(delta.newFields).map(n => n.toLowerCase()).join('-'));
195
- if (delta.newRelationships && delta.newRelationships.length > 0)
196
- descParts.push('add-relations');
197
- }
198
-
199
- const changelogFileName = `changelog-${dateStamp}-${descParts.join('__')}.xml`;
200
- const changelogPath = path.join(changelogsDir, changelogFileName);
201
- const masterPath = path.join(liquibaseDir, 'master.xml');
202
-
203
- // Write the changelog XML
204
- const changelogXml = `<?xml version="1.0" encoding="UTF-8"?>
205
- <databaseChangeLog
206
- xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
207
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
208
- xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
209
- http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
210
- ${changeSets}
211
- </databaseChangeLog>
212
- `;
213
- await fs.writeFile(changelogPath, changelogXml);
214
-
215
- // Register in master.xml (only now that the file exists)
216
- let masterXml = '';
217
- if (await fs.pathExists(masterPath)) {
218
- masterXml = await fs.readFile(masterPath, 'utf8');
219
- if (!masterXml.includes(changelogFileName)) {
220
- const includeLine = ` <include file="changelogs/${changelogFileName}" relativeToChangelogFile="true"/>`;
221
- masterXml = masterXml.replace('</databaseChangeLog>', `${includeLine}\n</databaseChangeLog>`);
222
- }
125
+ descParts.push('initial_schema');
223
126
  } else {
224
- masterXml = `<?xml version="1.0" encoding="UTF-8"?>
225
- <databaseChangeLog
226
- xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
227
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
228
- xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
229
- http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
230
-
231
- <include file="changelogs/${changelogFileName}" relativeToChangelogFile="true"/>
232
- </databaseChangeLog>
233
- `;
127
+ if (delta.newEntities?.length > 0) descParts.push('create_' + delta.newEntities.map(e => e.name.toLowerCase()).join('_'));
128
+ if (delta.newFields) descParts.push('add_fields');
234
129
  }
235
- await fs.writeFile(masterPath, masterXml);
236
130
 
237
- console.log(chalk.gray(` Generated Incremental Changelog: ${changelogFileName}`));
131
+ const fileName = `${timestamp}_${descParts.join('_')}.sql`;
132
+ await fs.writeFile(path.join(sqlDir, fileName), sqlUp + sqlDown);
133
+ console.log(chalk.gray(` Generated Goose SQL Migration: ${fileName}`));
238
134
  };
239
-
240
- const _unused = null; // toLiquibaseType imported from parser/gdl.js
@@ -6,29 +6,16 @@ export const generateMQTTCode = async (config, outputDir) => {
6
6
  const messagingDir = path.join(outputDir, 'messaging');
7
7
  await fs.ensureDir(messagingDir);
8
8
 
9
- const mqttClientGo = `
10
- package messaging
9
+ const mqttClientGo = `package messaging
11
10
 
12
11
  import (
13
- "encoding/json"
14
- "fmt"
15
12
  "log"
16
- "time"
17
-
18
13
  "{{app_name}}/config"
19
14
  mq "github.com/eclipse/paho.mqtt.golang"
20
15
  )
21
16
 
22
17
  var MQTTClient mq.Client
23
18
 
24
- type EventMessage struct {
25
- Action string \`json:"action"\`
26
- Entity string \`json:"entity"\`
27
- EventTime time.Time \`json:"event_time"\`
28
- Payload interface{} \`json:"payload"\`
29
- PreviousValue interface{} \`json:"previous_value,omitempty"\`
30
- }
31
-
32
19
  func InitMQTT(cfg *config.Config) {
33
20
  if !cfg.GoDuck.Messaging.MQTT.Enabled {
34
21
  log.Println("MQTT Messaging is disabled.")
@@ -56,32 +43,8 @@ func InitMQTT(cfg *config.Config) {
56
43
 
57
44
  MQTTClient = client
58
45
  }
59
-
60
- func PublishEvent(topicPrefix string, action string, entity string, payload interface{}, prev interface{}) {
61
- if MQTTClient == nil || !MQTTClient.IsConnected() {
62
- return
63
- }
64
-
65
- msg := EventMessage{
66
- Action: action,
67
- Entity: entity,
68
- EventTime: time.Now(),
69
- Payload: payload,
70
- PreviousValue: prev,
71
- }
72
-
73
- data, err := json.Marshal(msg)
74
- if err != nil {
75
- log.Printf("Error marshaling MQTT message: %v", err)
76
- return
77
- }
78
-
79
- topic := fmt.Sprintf("%s/%s/%s", topicPrefix, entity, action)
80
- token := MQTTClient.Publish(topic, 0, false, data)
81
- token.Wait()
82
- }
83
46
  `;
84
47
 
85
48
  await fs.writeFile(path.join(messagingDir, 'mqtt.go'), mqttClientGo.replace(/{{app_name}}/g, config.name));
86
- console.log(chalk.gray(' Generated MQTT Messaging Package'));
49
+ console.log(chalk.gray(' Generated MQTT Connection Client'));
87
50
  };