@restforgejs/platform 5.2.16 → 5.3.5

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 (199) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +1 -1
  3. package/cli/consumer.js +1 -1
  4. package/generators/cli/endpoint/create.js +69 -6
  5. package/generators/cli/payload/sync.js +16 -6
  6. package/generators/cli/project/auth.js +2 -2
  7. package/generators/cli/project/sdk.js +112 -0
  8. package/generators/lib/arg-parser.js +6 -0
  9. package/generators/lib/auth/processor-generator.js +5 -3
  10. package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
  11. package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
  12. package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
  13. package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
  14. package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
  15. package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
  16. package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
  17. package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
  18. package/generators/lib/generators/model-generator.js +46 -59
  19. package/generators/lib/help-generator.js +41 -3
  20. package/generators/lib/payload/endpoint-schema-validator.js +8 -3
  21. package/generators/lib/payload/field-projections.js +116 -0
  22. package/generators/lib/payload/payload-runner.js +164 -48
  23. package/generators/lib/payload/schema-diff.js +108 -0
  24. package/generators/lib/sdk/generator.js +719 -0
  25. package/generators/lib/sdk/naming.js +48 -0
  26. package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
  27. package/generators/lib/sdk/runtime/auth-client.js +186 -0
  28. package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
  29. package/generators/lib/sdk/runtime/http-client.js +81 -0
  30. package/generators/lib/sdk/runtime/resource-client.js +59 -0
  31. package/generators/lib/sdk/runtime/storage.js +31 -0
  32. package/generators/lib/templates/dashboard-catalog.js +1 -1
  33. package/generators/lib/templates/db-connection-env.js +1 -1
  34. package/generators/lib/templates/dbschema-catalog.js +1 -1
  35. package/generators/lib/templates/field-validation-catalog.js +1 -1
  36. package/generators/lib/templates/mysql-template.js +1 -1
  37. package/generators/lib/templates/oracle-template.js +1 -1
  38. package/generators/lib/templates/postgres-template.js +1 -1
  39. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  40. package/generators/lib/templates/sqlite-template.js +1 -1
  41. package/generators/lib/utils/cli-output.js +40 -0
  42. package/generators/lib/utils/config-resolver.js +61 -0
  43. package/generators/lib/utils/database-introspector.js +28 -5
  44. package/integrity-manifest.json +18 -18
  45. package/package.json +1 -1
  46. package/scripts/verify-integrity.js +1 -1
  47. package/server.js +1 -1
  48. package/src/components/handlers/adjust_handler.js +1 -1
  49. package/src/components/handlers/audit_handler.js +1 -1
  50. package/src/components/handlers/delete_handler.js +1 -1
  51. package/src/components/handlers/export_handler.js +1 -1
  52. package/src/components/handlers/import_handler.js +1 -1
  53. package/src/components/handlers/insert_handler.js +1 -1
  54. package/src/components/handlers/update_handler.js +1 -1
  55. package/src/components/handlers/upload_handler.js +1 -1
  56. package/src/components/handlers/workflow_handler.js +1 -1
  57. package/src/components/integrations/webhook.js +1 -1
  58. package/src/consumers/baseConsumer.js +1 -1
  59. package/src/consumers/declarativeMapper.js +1 -1
  60. package/src/consumers/handlers/apiHandler.js +1 -1
  61. package/src/consumers/handlers/consoleHandler.js +1 -1
  62. package/src/consumers/handlers/databaseHandler.js +1 -1
  63. package/src/consumers/handlers/index.js +1 -1
  64. package/src/consumers/handlers/kafkaHandler.js +1 -1
  65. package/src/consumers/index.js +1 -1
  66. package/src/consumers/messageTransformer.js +1 -1
  67. package/src/consumers/validator.js +1 -1
  68. package/src/core/db/dialect/base-dialect.js +1 -1
  69. package/src/core/db/dialect/index.js +1 -1
  70. package/src/core/db/dialect/mysql-dialect.js +1 -1
  71. package/src/core/db/dialect/oracle-dialect.js +1 -1
  72. package/src/core/db/dialect/postgres-dialect.js +1 -1
  73. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  74. package/src/core/db/flatten-helper.js +1 -1
  75. package/src/core/db/query-builder-error.js +1 -1
  76. package/src/core/db/query-builder.js +1 -1
  77. package/src/core/db/relation-helper.js +1 -1
  78. package/src/core/handlers/delete_handler.js +1 -1
  79. package/src/core/handlers/insert_handler.js +1 -1
  80. package/src/core/handlers/update_handler.js +1 -1
  81. package/src/core/models/base-model.js +1 -1
  82. package/src/core/utils/cache-manager.js +1 -1
  83. package/src/core/utils/component-engine.js +1 -1
  84. package/src/core/utils/context-builder.js +1 -1
  85. package/src/core/utils/datetime-formatter.js +1 -1
  86. package/src/core/utils/datetime-parser.js +1 -1
  87. package/src/core/utils/db.js +1 -1
  88. package/src/core/utils/logger.js +1 -1
  89. package/src/core/utils/payload-loader.js +1 -1
  90. package/src/core/utils/security-checks.js +1 -1
  91. package/src/middleware/body-options.js +1 -1
  92. package/src/middleware/cors.js +1 -1
  93. package/src/middleware/idempotency.js +1 -1
  94. package/src/middleware/rate-limiter.js +1 -1
  95. package/src/middleware/request-logger.js +1 -1
  96. package/src/middleware/security-headers.js +1 -1
  97. package/src/models/base-model-mysql.js +1 -1
  98. package/src/models/base-model-oracle.js +1 -1
  99. package/src/models/base-model-sqlite.js +1 -1
  100. package/src/models/base-model.js +1 -1
  101. package/src/pro/caching/redis-client.js +1 -1
  102. package/src/pro/caching/redis-helper.js +1 -1
  103. package/src/pro/consumers/baseConsumer.js +1 -1
  104. package/src/pro/consumers/declarativeMapper.js +1 -1
  105. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  106. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  107. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  108. package/src/pro/consumers/handlers/index.js +1 -1
  109. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  110. package/src/pro/consumers/index.js +1 -1
  111. package/src/pro/consumers/messageTransformer.js +1 -1
  112. package/src/pro/consumers/validator.js +1 -1
  113. package/src/pro/database/base-model-mysql.js +1 -1
  114. package/src/pro/database/base-model-oracle.js +1 -1
  115. package/src/pro/database/base-model-sqlite.js +1 -1
  116. package/src/pro/database/db-mysql.js +1 -1
  117. package/src/pro/database/db-oracle.js +1 -1
  118. package/src/pro/database/db-sqlite.js +1 -1
  119. package/src/pro/excel/excel-generator.js +1 -1
  120. package/src/pro/excel/excel-parser.js +1 -1
  121. package/src/pro/excel/export-service.js +1 -1
  122. package/src/pro/excel/export_handler.js +1 -1
  123. package/src/pro/excel/import-service.js +1 -1
  124. package/src/pro/excel/import-validator.js +1 -1
  125. package/src/pro/excel/import_handler.js +1 -1
  126. package/src/pro/excel/upsert-builder.js +1 -1
  127. package/src/pro/idgen/idgen-routes.js +1 -1
  128. package/src/pro/integrations/lookup-resolver.js +1 -1
  129. package/src/pro/integrations/upload-handler-v2.js +1 -1
  130. package/src/pro/integrations/upload-handler.js +1 -1
  131. package/src/pro/integrations/webhook.js +1 -1
  132. package/src/pro/locking/lock-routes.js +1 -1
  133. package/src/pro/locking/resource-lock-manager.js +1 -1
  134. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  135. package/src/pro/messaging/kafkaService.js +1 -1
  136. package/src/pro/messaging/messagehubService.js +1 -1
  137. package/src/pro/messaging/rabbitmqService.js +1 -1
  138. package/src/pro/scheduler/job-manager.js +1 -1
  139. package/src/pro/scheduler/job-routes.js +1 -1
  140. package/src/pro/scheduler/job-validator.js +1 -1
  141. package/src/pro/storage/base-storage-provider.js +1 -1
  142. package/src/pro/storage/file-metadata-helper.js +1 -1
  143. package/src/pro/storage/index.js +1 -1
  144. package/src/pro/storage/local-storage-provider.js +1 -1
  145. package/src/pro/storage/s3-storage-provider.js +1 -1
  146. package/src/pro/storage/upload-cleanup-job.js +1 -1
  147. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  148. package/src/pro/storage/upload-pending-tracker.js +1 -1
  149. package/src/pro/websocket/broadcast-helper.js +1 -1
  150. package/src/pro/websocket/index.js +1 -1
  151. package/src/pro/websocket/livesync-server.js +1 -1
  152. package/src/pro/websocket/ws-broadcaster.js +1 -1
  153. package/src/services/export-service.js +1 -1
  154. package/src/services/import-service.js +1 -1
  155. package/src/services/kafkaConsumerService.js +1 -1
  156. package/src/services/kafkaService.js +1 -1
  157. package/src/services/messagehubService.js +1 -1
  158. package/src/services/rabbitmqService.js +1 -1
  159. package/src/utils/cache-invalidation-registry.js +1 -1
  160. package/src/utils/cache-manager.js +1 -1
  161. package/src/utils/component-engine.js +1 -1
  162. package/src/utils/config-extractor.js +1 -1
  163. package/src/utils/consumerLogger.js +1 -1
  164. package/src/utils/context-builder.js +1 -1
  165. package/src/utils/dashboard-helpers.js +1 -1
  166. package/src/utils/dateHelper.js +1 -1
  167. package/src/utils/datetime-formatter.js +1 -1
  168. package/src/utils/datetime-parser.js +1 -1
  169. package/src/utils/db-bootstrap.js +1 -1
  170. package/src/utils/db-mysql.js +1 -1
  171. package/src/utils/db-oracle.js +1 -1
  172. package/src/utils/db-sqlite.js +1 -1
  173. package/src/utils/db.js +1 -1
  174. package/src/utils/demo-generator.js +1 -1
  175. package/src/utils/excel-generator.js +1 -1
  176. package/src/utils/excel-parser.js +1 -1
  177. package/src/utils/file-watcher.js +1 -1
  178. package/src/utils/id-generator.js +1 -1
  179. package/src/utils/idempotency-manager.js +1 -1
  180. package/src/utils/import-validator.js +1 -1
  181. package/src/utils/license-client.js +1 -1
  182. package/src/utils/lock-manager.js +1 -1
  183. package/src/utils/logger.js +1 -1
  184. package/src/utils/lookup-resolver.js +1 -1
  185. package/src/utils/payload-loader.js +1 -1
  186. package/src/utils/processor-response.js +1 -1
  187. package/src/utils/rabbitmq.js +1 -1
  188. package/src/utils/redis-client.js +1 -1
  189. package/src/utils/redis-helper.js +1 -1
  190. package/src/utils/request-scope.js +1 -1
  191. package/src/utils/security-checks.js +1 -1
  192. package/src/utils/service-resolver.js +1 -1
  193. package/src/utils/shutdown-coordinator.js +1 -1
  194. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  195. package/src/utils/sql-table-extractor.js +1 -1
  196. package/src/utils/trusted-keys.js +1 -1
  197. package/src/utils/upload-handler.js +1 -1
  198. package/src/utils/upsert-builder.js +1 -1
  199. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,719 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generator SDK JavaScript untuk satu project RESTForge.
5
+ *
6
+ * Rujukan desain: docs/plan/sdk-generator-command.md (versi awal, TANPA auth).
7
+ *
8
+ * Sumber kebenaran tunggal = `metadata/<project>.json`:
9
+ * - key endpoint -> slug (= segment route nyata `/api/<project>/<slug>/<verb>`)
10
+ * - `actions` -> action map untuk generic verb builder (core/resource-client.js)
11
+ * - `type: module`-> hanya endpoint REST yang digenerate (processor/dashboard/consumer dilewati)
12
+ *
13
+ * `primaryKey` tidak ada di metadata, jadi diambil dari file payload
14
+ * `payload/<slug>.json`. Bila payload tidak ditemukan, SELURUH generate dibatalkan
15
+ * (tidak ada SDK parsial) — sesuai keputusan desain.
16
+ *
17
+ * Core generik (http-client.js, resource-client.js) identik antar project, jadi
18
+ * cukup disalin dari ./runtime/ tanpa template per-project.
19
+ */
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ const { toCamel, toPascal, toKebab } = require('./naming');
25
+ const { resolveConfig } = require('../utils/config-resolver');
26
+ const { readEnvFile } = require('../utils/env-manager');
27
+
28
+ const RUNTIME_DIR = path.resolve(__dirname, 'runtime');
29
+ const RUNTIME_FILES = ['http-client.js', 'resource-client.js'];
30
+ // Core tambahan, disalin HANYA bila project punya auth extension (project auth).
31
+ const AUTH_RUNTIME_FILES = ['auth-client.js', 'storage.js'];
32
+
33
+ /**
34
+ * Error generate dengan exit code eksplisit untuk dipakai cli-entry.js.
35
+ */
36
+ function generateError(message, exitCode = 1) {
37
+ const err = new Error(message);
38
+ err.exitCode = exitCode;
39
+ return err;
40
+ }
41
+
42
+ /**
43
+ * Tentukan lokasi metadata + payload. Mendukung dua layout:
44
+ * - flat (fast-track): metadata/payload langsung di root (cwd)
45
+ * - split: backend ada di `<cwd>/backend/`, metadata/payload di sana
46
+ *
47
+ * Output SDK default selalu sibling root-level (`<cwd>/sdk`), bukan di dalam backend/.
48
+ */
49
+ function resolveLayout(workingDir, project) {
50
+ const candidates = [
51
+ { baseDir: workingDir, label: 'flat' },
52
+ { baseDir: path.join(workingDir, 'backend'), label: 'split' }
53
+ ];
54
+
55
+ for (const candidate of candidates) {
56
+ const metadataPath = path.join(candidate.baseDir, 'metadata', `${project}.json`);
57
+ if (fs.existsSync(metadataPath)) {
58
+ return {
59
+ layout: candidate.label,
60
+ baseDir: candidate.baseDir,
61
+ metadataPath,
62
+ payloadDir: path.join(candidate.baseDir, 'payload')
63
+ };
64
+ }
65
+ }
66
+
67
+ throw generateError(
68
+ `Metadata for project '${project}' not found. Looked in:\n` +
69
+ candidates.map((c) => ` - ${path.join(c.baseDir, 'metadata', `${project}.json`)}`).join('\n') +
70
+ `\nMake sure the project has been generated (endpoint create / fast-track) before creating an SDK.`,
71
+ 1
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Baca metadata, kembalikan daftar endpoint REST (type module) beserta action map
77
+ * yang sudah difilter hanya yang aktif (true).
78
+ */
79
+ function readModuleEndpoints(metadataPath) {
80
+ let metadata;
81
+ try {
82
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
83
+ } catch (err) {
84
+ throw generateError(`Failed to read metadata ${metadataPath}: ${err.message}`, 1);
85
+ }
86
+
87
+ const endpoints = metadata && metadata.endpoints;
88
+ if (!endpoints || typeof endpoints !== 'object') {
89
+ throw generateError(`Metadata ${metadataPath} has no 'endpoints' field.`, 1);
90
+ }
91
+
92
+ const result = [];
93
+ for (const [slug, info] of Object.entries(endpoints)) {
94
+ if (!info || info.type !== 'module') continue;
95
+ if (!info.actions || typeof info.actions !== 'object') continue;
96
+
97
+ const activeAction = {};
98
+ for (const [verb, enabled] of Object.entries(info.actions)) {
99
+ if (enabled === true) activeAction[verb] = true;
100
+ }
101
+
102
+ result.push({
103
+ slug,
104
+ tableName: info.tableName || null,
105
+ action: activeAction
106
+ });
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Cari file payload untuk sebuah resource. Konvensi utama: `payload/<slug>.json`.
114
+ * Fallback ke nama berbasis tableName untuk berjaga-jaga.
115
+ */
116
+ function resolvePayloadFile(payloadDir, slug, tableName) {
117
+ const candidates = [`${slug}.json`];
118
+ if (tableName) {
119
+ candidates.push(`${tableName}.json`);
120
+ candidates.push(`${toKebab(tableName)}.json`);
121
+ }
122
+
123
+ for (const name of candidates) {
124
+ const full = path.join(payloadDir, name);
125
+ if (fs.existsSync(full)) return full;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Baca metadata payload yang dibutuhkan SDK: primaryKey + daftar field
132
+ * (dari fieldValidation). Abort bila payload/primaryKey tidak ada.
133
+ *
134
+ * @returns {{ primaryKey: string, fields: Array<{name,type,required,autoGenerate,primaryKey,enum,default,maxLength}> }}
135
+ */
136
+ function readPayloadMeta(payloadDir, endpoint) {
137
+ const payloadFile = resolvePayloadFile(payloadDir, endpoint.slug, endpoint.tableName);
138
+ if (!payloadFile) {
139
+ throw generateError(
140
+ `Payload for resource '${endpoint.slug}' not found in ${payloadDir} ` +
141
+ `(looked for ${endpoint.slug}.json` + (endpoint.tableName ? `, ${endpoint.tableName}.json` : '') + `). ` +
142
+ `Generation aborted to avoid producing a partial SDK.`,
143
+ 1
144
+ );
145
+ }
146
+
147
+ let payload;
148
+ try {
149
+ payload = JSON.parse(fs.readFileSync(payloadFile, 'utf8'));
150
+ } catch (err) {
151
+ throw generateError(`Failed to read payload ${payloadFile}: ${err.message}`, 1);
152
+ }
153
+
154
+ if (!payload.primaryKey || typeof payload.primaryKey !== 'string') {
155
+ throw generateError(
156
+ `Payload ${payloadFile} has no valid 'primaryKey' field. Generation aborted.`,
157
+ 1
158
+ );
159
+ }
160
+
161
+ const fields = Array.isArray(payload.fieldValidation)
162
+ ? payload.fieldValidation.map((f) => {
163
+ const c = (f && f.constraints) || {};
164
+ return {
165
+ name: f.name,
166
+ type: f.type || 'string',
167
+ required: c.required === true,
168
+ autoGenerate: c.autoGenerate === true,
169
+ primaryKey: c.primaryKey === true,
170
+ enum: Array.isArray(c.enum) ? c.enum : null,
171
+ default: Object.prototype.hasOwnProperty.call(c, 'default') ? c.default : undefined,
172
+ maxLength: typeof c.maxLength === 'number' ? c.maxLength : null
173
+ };
174
+ }).filter((f) => f.name)
175
+ : [];
176
+
177
+ return { primaryKey: payload.primaryKey, fields };
178
+ }
179
+
180
+ /**
181
+ * Wrapper kompatibilitas: hanya primaryKey.
182
+ */
183
+ function readPrimaryKey(payloadDir, endpoint) {
184
+ return readPayloadMeta(payloadDir, endpoint).primaryKey;
185
+ }
186
+
187
+ function renderActionLiteral(action) {
188
+ const keys = Object.keys(action);
189
+ if (keys.length === 0) return '{}';
190
+ const lines = keys.map((key) => ` ${key}: true`);
191
+ return '{\n' + lines.join(',\n') + '\n }';
192
+ }
193
+
194
+ function renderResourceFile(resource) {
195
+ const factory = `create${toPascal(resource.slug)}Resource`;
196
+ return `// Auto-generated dari payload ${resource.slug}.json oleh \`restforge project sdk\` — JANGAN diedit manual.
197
+ // Descriptor (primaryKey, action) disalin dari payload RDF backend; slug = nama endpoint terdeploy.
198
+
199
+ import { createResource } from '../core/resource-client.js';
200
+
201
+ export function ${factory}(http) {
202
+ return createResource(http, {
203
+ slug: '${resource.slug}',
204
+ primaryKey: '${resource.primaryKey}',
205
+ action: ${renderActionLiteral(resource.action)}
206
+ });
207
+ }
208
+ `;
209
+ }
210
+
211
+ function renderIndexFile(project, resources, hasAuth) {
212
+ const firstKey = resources.length > 0 ? resources[0].clientKey : 'resource';
213
+ const imports = resources
214
+ .map((r) => `import { ${r.factory} } from './resources/${r.slug}.js';`)
215
+ .join('\n');
216
+ const assignments = resources
217
+ .map((r) => ` ${r.clientKey}: ${r.factory}(http)`)
218
+ .join(',\n');
219
+
220
+ if (!hasAuth) {
221
+ return `// Entrypoint tunggal SDK ${project}. Auto-generated oleh \`restforge project sdk\` — JANGAN diedit manual.
222
+ //
223
+ // Project ini TANPA auth extension: createClient hanya butuh baseUrl.
224
+ //
225
+ // Pemakaian:
226
+ // import { createClient } from '${project}';
227
+ // const client = createClient({ baseUrl: 'http://127.0.0.1:3000/api/${project}' });
228
+ // const result = await client.${firstKey}.datatables({ start: 0, length: 10 });
229
+
230
+ import { HttpClient } from './core/http-client.js';
231
+
232
+ ${imports}
233
+
234
+ export function createClient({ baseUrl } = {}) {
235
+ if (!baseUrl) throw new Error('createClient: baseUrl wajib diisi');
236
+
237
+ const http = new HttpClient({ baseUrl });
238
+
239
+ const client = {
240
+ ${assignments}
241
+ };
242
+
243
+ return client;
244
+ }
245
+
246
+ export { ApiError } from './core/http-client.js';
247
+ `;
248
+ }
249
+
250
+ return `// Entrypoint tunggal SDK ${project}. Auto-generated oleh \`restforge project sdk\` — JANGAN diedit manual.
251
+ //
252
+ // Project ini PUNYA auth extension (project auth). Endpoint auth di-mount di {baseUrl}/rfx_auth/*.
253
+ // client.auth menyediakan login/logout/refresh/getMe; token otomatis dilampirkan ke setiap
254
+ // pemanggilan resource (Bearer) dan di-refresh saat 401.
255
+ //
256
+ // Pemakaian:
257
+ // import { createClient } from '${project}';
258
+ // const client = createClient({ baseUrl: 'http://127.0.0.1:3000/api/${project}' });
259
+ // await client.auth.login('username', 'password');
260
+ // const result = await client.${firstKey}.datatables({ start: 0, length: 10 });
261
+
262
+ import { HttpClient } from './core/http-client.js';
263
+ import { createAuthClient } from './core/auth-client.js';
264
+ import { createLocalStorageAdapter } from './core/storage.js';
265
+
266
+ ${imports}
267
+
268
+ export function createClient({ baseUrl, storage } = {}) {
269
+ if (!baseUrl) throw new Error('createClient: baseUrl wajib diisi');
270
+
271
+ const resolvedStorage = storage || createLocalStorageAdapter();
272
+ const auth = createAuthClient({ baseUrl: baseUrl + '/rfx_auth', storage: resolvedStorage });
273
+
274
+ const http = new HttpClient({
275
+ baseUrl,
276
+ getAccessToken: () => auth.getAccessToken(),
277
+ refreshAccessToken: () => auth.refresh(),
278
+ onSessionExpired: () => auth.logout()
279
+ });
280
+
281
+ const client = {
282
+ ${assignments}
283
+ };
284
+
285
+ client.auth = auth;
286
+
287
+ return client;
288
+ }
289
+
290
+ export { ApiError } from './core/http-client.js';
291
+ export { createLocalStorageAdapter, createMemoryStorageAdapter } from './core/storage.js';
292
+ `;
293
+ }
294
+
295
+ function renderPackageJson(project) {
296
+ const pkg = {
297
+ name: project,
298
+ version: '0.1.0',
299
+ description: `SDK client untuk REST API ${project} (RESTForge).`,
300
+ type: 'module',
301
+ main: 'dist/index.cjs',
302
+ module: 'dist/index.js',
303
+ browser: 'dist/index.global.js',
304
+ exports: {
305
+ '.': {
306
+ import: './dist/index.js',
307
+ require: './dist/index.cjs'
308
+ }
309
+ },
310
+ files: ['dist/'],
311
+ scripts: {
312
+ build: 'tsup',
313
+ deploy: 'node deploy.mjs'
314
+ },
315
+ devDependencies: {
316
+ // tsup melakukan require('typescript') saat startup (bukan hanya untuk .d.ts),
317
+ // jadi typescript wajib ada walau source SDK murni JavaScript.
318
+ tsup: '^8.0.0',
319
+ typescript: '^6.0.3'
320
+ }
321
+ };
322
+ return JSON.stringify(pkg, null, 4) + '\n';
323
+ }
324
+
325
+ /**
326
+ * Tentukan baseUrl REST API untuk di-bake ke sdk-client.js.
327
+ * Prioritas: override eksplisit -> default config (.restforge/defaults.json -> env) -> fallback.
328
+ * SERVER_ADDRESS 0.0.0.0/kosong dipetakan ke 127.0.0.1 (tidak bisa dipakai browser).
329
+ */
330
+ function resolveBaseUrl({ workingDir, project, override = null, log = () => {} }) {
331
+ const fallback = `http://127.0.0.1:3000/api/${project}`;
332
+ if (override) return override;
333
+
334
+ try {
335
+ const resolved = resolveConfig(null, workingDir);
336
+ if (resolved && fs.existsSync(resolved.path)) {
337
+ const { data } = readEnvFile(resolved.path);
338
+ let host = (data.SERVER_ADDRESS || '').trim();
339
+ if (!host || host === '0.0.0.0') host = '127.0.0.1';
340
+ const port = (data.SERVER_PORT || '3000').trim();
341
+ return `http://${host}:${port}/api/${project}`;
342
+ }
343
+ } catch (_e) {
344
+ // fall through ke fallback
345
+ }
346
+
347
+ log(`Default config not found — baseUrl falls back to ${fallback}`);
348
+ return fallback;
349
+ }
350
+
351
+ function renderBootstrapClient(project, baseUrl, hasAuth) {
352
+ const globalVar = toCamel(project);
353
+ const authNote = hasAuth
354
+ ? `// Auth extension detected: window.${globalVar}.auth is available (login/logout/refresh).`
355
+ : `// Generated WITHOUT auth: window.${globalVar}.auth does not exist.`;
356
+ return `// Bootstrap for the ${project} SDK — auto-generated by \`restforge project sdk\`.
357
+ // Load this as <script type="module"> BEFORE other classic scripts.
358
+ ${authNote}
359
+ //
360
+ // After deploy this file lives in <app>/js/ and the SDK in <app>/js/sdk/.
361
+ import { createClient } from './sdk/index.js';
362
+
363
+ window.${globalVar} = createClient({ baseUrl: '${baseUrl}' });
364
+
365
+ document.dispatchEvent(new Event('${project}:ready'));
366
+ `;
367
+ }
368
+
369
+ function renderDeployScript(project) {
370
+ const template = fs.readFileSync(path.join(RUNTIME_DIR, 'deploy.mjs.tmpl'), 'utf8');
371
+ return template
372
+ .replace(/__PROJECT_GLOBAL__/g, toCamel(project))
373
+ .replace(/__PROJECT__/g, project);
374
+ }
375
+
376
+ /**
377
+ * Daftar nama method client untuk sebuah action map, MENGIKUTI pemetaan
378
+ * core/resource-client.js: workflow -> changeStatus, export/import dilewati,
379
+ * dan lookup aktif menambah lookupDynamic. Dipakai kolom Methods di README.
380
+ */
381
+ function resourceMethods(action = {}) {
382
+ const SKIP = new Set(['export', 'import']);
383
+ const OVERRIDE = { workflow: 'changeStatus' };
384
+ const methods = [];
385
+ for (const [verb, enabled] of Object.entries(action)) {
386
+ if (!enabled || SKIP.has(verb)) continue;
387
+ methods.push(OVERRIDE[verb] || verb);
388
+ }
389
+ if (action.lookup) methods.push('lookupDynamic');
390
+ return methods;
391
+ }
392
+
393
+ /**
394
+ * Catatan ringkas constraint sebuah field untuk kolom Notes di tabel Fields.
395
+ */
396
+ function fieldNote(field) {
397
+ const notes = [];
398
+ if (field.primaryKey) notes.push('primary key');
399
+ if (field.autoGenerate) notes.push('auto-generated');
400
+ if (field.enum) notes.push(`enum: ${field.enum.join(', ')}`);
401
+ if (field.maxLength) notes.push(`max ${field.maxLength}`);
402
+ if (field.default !== undefined) notes.push(`default: ${field.default}`);
403
+ return notes.join('; ');
404
+ }
405
+
406
+ // Kolom audit diisi runtime, tidak dikirim user.
407
+ const AUDIT_COLUMNS = new Set(['created_at', 'created_by', 'updated_at', 'updated_by']);
408
+
409
+ /**
410
+ * Field yang relevan dikirim user saat create. Mengikuti prototype: yang dikecualikan
411
+ * hanya primary key auto-generated dan kolom audit. Field autoGenerate non-PK (mis.
412
+ * tanggal default) TETAP boleh dikirim, jadi ikut ditampilkan.
413
+ */
414
+ function writableFields(fields) {
415
+ return fields.filter((f) => {
416
+ if (f.primaryKey && f.autoGenerate) return false;
417
+ if (AUDIT_COLUMNS.has(f.name)) return false;
418
+ return true;
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Nilai contoh placeholder per field (berdasar tipe / enum).
424
+ */
425
+ function exampleValue(field) {
426
+ if (field.enum && field.enum.length > 0) return `'${field.enum[0]}'`;
427
+ switch (field.type) {
428
+ case 'integer':
429
+ case 'number':
430
+ case 'decimal':
431
+ case 'float':
432
+ return '0';
433
+ case 'boolean':
434
+ return 'true';
435
+ case 'date':
436
+ case 'datetime':
437
+ case 'timestamp':
438
+ return "'2026-01-01 00:00'";
439
+ default:
440
+ return "'text'";
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Object literal contoh untuk pemanggilan create resource pertama.
446
+ */
447
+ function createExampleBody(fields) {
448
+ const writable = writableFields(fields);
449
+ if (writable.length === 0) return '{ /* fields */ }';
450
+ const lines = writable.map((f) => ` ${f.name}: ${exampleValue(f)}`);
451
+ return '{\n' + lines.join(',\n') + '\n}';
452
+ }
453
+
454
+ /**
455
+ * Section "Fields per Resource": satu sub-tabel per resource dari fieldValidation.
456
+ */
457
+ function renderFieldsSection(resources) {
458
+ return resources.map((r) => {
459
+ const heading = `### \`${r.slug}\` (\`client.${r.clientKey}\`)`;
460
+ if (!r.fields || r.fields.length === 0) {
461
+ return `${heading}\n\n_Field tidak dideklarasikan di payload._`;
462
+ }
463
+ const rows = r.fields.map((f) => {
464
+ const note = fieldNote(f);
465
+ return `| \`${f.name}\` | ${f.type} | ${f.required ? 'yes' : 'no'} | ${note} |`;
466
+ }).join('\n');
467
+ return `${heading}\n\n| Field | Type | Required | Notes |\n|-------|------|----------|-------|\n${rows}`;
468
+ }).join('\n\n');
469
+ }
470
+
471
+ function renderAuthOverview(hasAuth) {
472
+ return hasAuth
473
+ ? 'enabled via `client.auth` (tokens attached automatically, refreshed on 401).'
474
+ : 'not included (project has no auth extension); `createClient` only accepts `baseUrl`.';
475
+ }
476
+
477
+ function renderAuthSection(project, hasAuth) {
478
+ if (!hasAuth) return '';
479
+ const g = toCamel(project);
480
+ return `## Authentication
481
+
482
+ This project has the auth extension (\`project auth\`). The client exposes \`client.auth\`, and
483
+ every resource call automatically attaches \`Authorization: Bearer <token>\` and retries once
484
+ after refreshing on a 401. Auth endpoints are mounted under \`<baseUrl>/rfx_auth\`.
485
+
486
+ \`\`\`js
487
+ // log in (tokens stored via the storage adapter; localStorage by default)
488
+ await window.${g}.auth.login('username', 'password');
489
+
490
+ // or sign in with Google (pass the ID token from Google Identity Services)
491
+ // await window.${g}.auth.google(googleIdToken);
492
+
493
+ // authenticated resource calls just work afterwards
494
+ // ...getResource().datatables(...)
495
+
496
+ // session helpers
497
+ window.${g}.auth.isAuthenticated(); // boolean
498
+ window.${g}.auth.getCurrentUser(); // cached user (or null)
499
+ await window.${g}.auth.getMe(); // fetch fresh profile
500
+
501
+ // log out (clears local tokens, invalidates refresh token on server)
502
+ window.${g}.auth.logout();
503
+ \`\`\`
504
+
505
+ ### Auth methods
506
+
507
+ | Method | Endpoint |
508
+ |--------|----------|
509
+ | \`auth.login(username, password)\` | \`POST /rfx_auth/login\` |
510
+ | \`auth.google(credential)\` | \`POST /rfx_auth/google\` (Google ID token) |
511
+ | \`auth.register({ username, password, email?, full_name? })\` | \`POST /rfx_auth/register\` |
512
+ | \`auth.refresh()\` | \`POST /rfx_auth/refresh\` (auto on 401) |
513
+ | \`auth.logout()\` | \`POST /rfx_auth/logout\` |
514
+ | \`auth.getMe()\` | \`GET /rfx_auth/me\` |
515
+ | \`auth.resetPassword({ email, new_password, confirm_password })\` | \`POST /rfx_auth/reset-password\` |
516
+ | \`auth.getAccessToken()\` / \`getRefreshToken()\` / \`getCurrentUser()\` / \`isAuthenticated()\` | local accessors |
517
+
518
+ Tokens are stored under \`auth_access_token\`, \`auth_refresh_token\`, \`auth_user\`. For SSR or
519
+ non-browser environments, pass a custom \`storage\` adapter:
520
+ \`createClient({ baseUrl, storage })\` (see \`createMemoryStorageAdapter\`).
521
+
522
+ `;
523
+ }
524
+
525
+ function renderReadme(project, resources, baseUrl, hasAuth) {
526
+ const template = fs.readFileSync(path.join(RUNTIME_DIR, 'README.md.tmpl'), 'utf8');
527
+ const first = resources[0];
528
+ const tableRows = resources
529
+ .map((r) => {
530
+ const methods = resourceMethods(r.action).join(', ') || '—';
531
+ return `| \`${r.slug}\` | \`client.${r.clientKey}\` | \`${r.primaryKey}\` | ${methods} |`;
532
+ })
533
+ .join('\n');
534
+ const treeLines = resources
535
+ .map((r, i) => ` │ ${i === resources.length - 1 ? '└──' : '├──'} ${r.slug}.js`)
536
+ .join('\n');
537
+ const coreFiles = hasAuth
538
+ ? ['http-client.js', 'resource-client.js', 'auth-client.js', 'storage.js']
539
+ : ['http-client.js', 'resource-client.js'];
540
+ const coreTree = coreFiles
541
+ .map((f, i) => ` │ ${i === coreFiles.length - 1 ? '└──' : '├──'} ${f}`)
542
+ .join('\n');
543
+
544
+ return template
545
+ .replace(/__RESOURCE_TABLE__/g, tableRows)
546
+ .replace(/__RESOURCE_TREE__/g, treeLines)
547
+ .replace(/__CORE_TREE__/g, coreTree)
548
+ .replace(/__RESOURCE_FIELDS__/g, renderFieldsSection(resources))
549
+ .replace(/__FIRST_CREATE_BODY__/g, createExampleBody(first.fields || []))
550
+ .replace(/__AUTH_OVERVIEW__/g, renderAuthOverview(hasAuth))
551
+ .replace(/__AUTH_SECTION__/g, renderAuthSection(project, hasAuth))
552
+ .replace(/__GLOBAL__/g, toCamel(project))
553
+ .replace(/__BASEURL__/g, baseUrl)
554
+ .replace(/__FIRST_KEY__/g, first.clientKey)
555
+ .replace(/__FIRST_PK__/g, first.primaryKey)
556
+ .replace(/__PROJECT__/g, project);
557
+ }
558
+
559
+ function renderTsupConfig(project) {
560
+ const globalName = toPascal(project);
561
+ return `import { defineConfig } from 'tsup';
562
+
563
+ // Entry utama: 3 format sekaligus (ESM untuk bundler modern, CJS untuk Node/SSR,
564
+ // IIFE/global untuk <script> classic biasa — window.${globalName}).
565
+ export default defineConfig([
566
+ {
567
+ entry: { index: 'src/index.js' },
568
+ format: ['esm', 'cjs', 'iife'],
569
+ globalName: '${globalName}',
570
+ outDir: 'dist',
571
+ clean: true,
572
+ sourcemap: false,
573
+ minify: false
574
+ }
575
+ ]);
576
+ `;
577
+ }
578
+
579
+ function copyRuntime(coreDir, hasAuth) {
580
+ const files = hasAuth ? RUNTIME_FILES.concat(AUTH_RUNTIME_FILES) : RUNTIME_FILES;
581
+ for (const file of files) {
582
+ const src = path.join(RUNTIME_DIR, file);
583
+ const dest = path.join(coreDir, file);
584
+ fs.copyFileSync(src, dest);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Deteksi apakah project sudah punya auth extension (project auth).
590
+ * Sinyal paling deterministik: file router rfx_auth + folder processor auth di src/modules/<project>/.
591
+ */
592
+ function detectAuth(baseDir, project) {
593
+ const moduleDir = path.join(baseDir, 'src', 'modules', project);
594
+ const routerFile = path.join(moduleDir, 'rfx_auth.js');
595
+ const processorDir = path.join(moduleDir, 'processor', 'auth');
596
+ return fs.existsSync(routerFile) || fs.existsSync(processorDir);
597
+ }
598
+
599
+ /**
600
+ * Generate SDK source ke `outputDir`.
601
+ *
602
+ * @param {object} options
603
+ * @param {string} options.workingDir - root tempat command dijalankan (umumnya process.cwd())
604
+ * @param {string} options.project - nama project (= nama package SDK)
605
+ * @param {string} [options.sdkPath] - override folder output (relatif ke workingDir atau absolut)
606
+ * @param {boolean} [options.force] - timpa SDK source yang sudah ada
607
+ * @param {function} [options.log] - sink log (default console.log)
608
+ * @returns {{ outputDir: string, layout: string, resources: string[] }}
609
+ */
610
+ function generateSdk(options = {}) {
611
+ const {
612
+ workingDir,
613
+ project,
614
+ sdkPath = null,
615
+ force = false,
616
+ log = console.log
617
+ } = options;
618
+
619
+ if (!workingDir) throw generateError('generateSdk: workingDir is required', 1);
620
+ if (!project) throw generateError('generateSdk: project is required', 1);
621
+
622
+ const baseUrl = options.baseUrl || `http://127.0.0.1:3000/api/${project}`;
623
+
624
+ const { layout, baseDir, metadataPath, payloadDir } = resolveLayout(workingDir, project);
625
+ log(`Detected layout : ${layout}`);
626
+ log(`Metadata : ${metadataPath}`);
627
+
628
+ const hasAuth = detectAuth(baseDir, project);
629
+ log(`Auth extension : ${hasAuth ? 'detected (rfx_auth) — SDK includes client.auth' : 'none'}`);
630
+
631
+ const endpoints = readModuleEndpoints(metadataPath);
632
+ if (endpoints.length === 0) {
633
+ throw generateError(
634
+ `No REST endpoints (type module) in metadata for project '${project}'. Nothing to generate.`,
635
+ 1
636
+ );
637
+ }
638
+
639
+ // Lengkapi tiap endpoint dengan primaryKey + field + nama turunan (abort bila payload hilang).
640
+ const resources = endpoints.map((endpoint) => {
641
+ const { primaryKey, fields } = readPayloadMeta(payloadDir, endpoint);
642
+ return {
643
+ slug: endpoint.slug,
644
+ primaryKey,
645
+ fields,
646
+ action: endpoint.action,
647
+ clientKey: toCamel(endpoint.slug),
648
+ factory: `create${toPascal(endpoint.slug)}Resource`
649
+ };
650
+ });
651
+
652
+ const outputDir = sdkPath
653
+ ? path.resolve(workingDir, sdkPath)
654
+ : path.join(workingDir, 'sdk');
655
+
656
+ const indexMarker = path.join(outputDir, 'src', 'index.js');
657
+ if (fs.existsSync(indexMarker) && !force) {
658
+ throw generateError(
659
+ `An SDK already exists at ${outputDir} (found src/index.js). Use --force to overwrite.`,
660
+ 1
661
+ );
662
+ }
663
+
664
+ const srcDir = path.join(outputDir, 'src');
665
+ const coreDir = path.join(srcDir, 'core');
666
+ const resourcesDir = path.join(srcDir, 'resources');
667
+
668
+ fs.mkdirSync(coreDir, { recursive: true });
669
+ fs.mkdirSync(resourcesDir, { recursive: true });
670
+
671
+ // Core generik (identik antar project) — salin dari runtime (+ auth bila terdeteksi).
672
+ copyRuntime(coreDir, hasAuth);
673
+ log(`Core copied : ${(hasAuth ? RUNTIME_FILES.concat(AUTH_RUNTIME_FILES) : RUNTIME_FILES).join(', ')}`);
674
+
675
+ // Resource per endpoint.
676
+ for (const resource of resources) {
677
+ const file = path.join(resourcesDir, `${resource.slug}.js`);
678
+ fs.writeFileSync(file, renderResourceFile(resource), 'utf8');
679
+ log(`Resource : ${resource.slug} (primaryKey=${resource.primaryKey})`);
680
+ }
681
+
682
+ // Entrypoint + build config.
683
+ fs.writeFileSync(path.join(srcDir, 'index.js'), renderIndexFile(project, resources, hasAuth), 'utf8');
684
+ fs.writeFileSync(path.join(outputDir, 'package.json'), renderPackageJson(project), 'utf8');
685
+ fs.writeFileSync(path.join(outputDir, 'tsup.config.js'), renderTsupConfig(project), 'utf8');
686
+ fs.writeFileSync(path.join(outputDir, 'deploy.mjs'), renderDeployScript(project), 'utf8');
687
+ fs.writeFileSync(path.join(outputDir, 'sdk-client.js'), renderBootstrapClient(project, baseUrl, hasAuth), 'utf8');
688
+ log(`Bootstrap : sdk-client.js (baseUrl=${baseUrl})`);
689
+ fs.writeFileSync(path.join(outputDir, 'README.md'), renderReadme(project, resources, baseUrl, hasAuth), 'utf8');
690
+ log(`Readme : README.md`);
691
+
692
+ return {
693
+ outputDir,
694
+ layout,
695
+ baseUrl,
696
+ hasAuth,
697
+ resources: resources.map((r) => r.slug)
698
+ };
699
+ }
700
+
701
+ module.exports = {
702
+ generateSdk,
703
+ resolveLayout,
704
+ readModuleEndpoints,
705
+ resolvePayloadFile,
706
+ readPrimaryKey,
707
+ readPayloadMeta,
708
+ detectAuth,
709
+ renderResourceFile,
710
+ renderIndexFile,
711
+ renderPackageJson,
712
+ renderTsupConfig,
713
+ renderDeployScript,
714
+ renderBootstrapClient,
715
+ renderReadme,
716
+ resourceMethods,
717
+ resolveBaseUrl,
718
+ generateError
719
+ };