@restforgejs/platform 5.2.16 → 5.3.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.
Files changed (203) hide show
  1. package/bin/restforge-designer-linux +0 -0
  2. package/bin/restforge-designer.exe +0 -0
  3. package/build-info.json +2 -2
  4. package/cli/consumer-deploy.js +1 -1
  5. package/cli/consumer.js +1 -1
  6. package/cli/designer.js +3 -0
  7. package/generators/cli/endpoint/create.js +69 -6
  8. package/generators/cli/fast-track.js +7 -7
  9. package/generators/cli/payload/sync.js +16 -6
  10. package/generators/cli/project/auth.js +2 -2
  11. package/generators/cli/project/sdk.js +112 -0
  12. package/generators/lib/arg-parser.js +6 -0
  13. package/generators/lib/auth/processor-generator.js +5 -3
  14. package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
  15. package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
  16. package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
  17. package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
  18. package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
  19. package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
  20. package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
  21. package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
  22. package/generators/lib/generators/model-generator.js +46 -59
  23. package/generators/lib/help-generator.js +41 -3
  24. package/generators/lib/payload/endpoint-schema-validator.js +8 -3
  25. package/generators/lib/payload/field-projections.js +116 -0
  26. package/generators/lib/payload/payload-runner.js +164 -48
  27. package/generators/lib/payload/schema-diff.js +108 -0
  28. package/generators/lib/sdk/generator.js +719 -0
  29. package/generators/lib/sdk/naming.js +48 -0
  30. package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
  31. package/generators/lib/sdk/runtime/auth-client.js +186 -0
  32. package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
  33. package/generators/lib/sdk/runtime/http-client.js +81 -0
  34. package/generators/lib/sdk/runtime/resource-client.js +59 -0
  35. package/generators/lib/sdk/runtime/storage.js +31 -0
  36. package/generators/lib/templates/dashboard-catalog.js +1 -1
  37. package/generators/lib/templates/db-connection-env.js +1 -1
  38. package/generators/lib/templates/dbschema-catalog.js +1 -1
  39. package/generators/lib/templates/field-validation-catalog.js +1 -1
  40. package/generators/lib/templates/mysql-template.js +1 -1
  41. package/generators/lib/templates/oracle-template.js +1 -1
  42. package/generators/lib/templates/postgres-template.js +1 -1
  43. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  44. package/generators/lib/templates/sqlite-template.js +1 -1
  45. package/generators/lib/utils/cli-output.js +40 -0
  46. package/generators/lib/utils/config-resolver.js +61 -0
  47. package/generators/lib/utils/database-introspector.js +28 -5
  48. package/integrity-manifest.json +18 -18
  49. package/package.json +3 -2
  50. package/scripts/verify-integrity.js +1 -1
  51. package/server.js +1 -1
  52. package/src/components/handlers/adjust_handler.js +1 -1
  53. package/src/components/handlers/audit_handler.js +1 -1
  54. package/src/components/handlers/delete_handler.js +1 -1
  55. package/src/components/handlers/export_handler.js +1 -1
  56. package/src/components/handlers/import_handler.js +1 -1
  57. package/src/components/handlers/insert_handler.js +1 -1
  58. package/src/components/handlers/update_handler.js +1 -1
  59. package/src/components/handlers/upload_handler.js +1 -1
  60. package/src/components/handlers/workflow_handler.js +1 -1
  61. package/src/components/integrations/webhook.js +1 -1
  62. package/src/consumers/baseConsumer.js +1 -1
  63. package/src/consumers/declarativeMapper.js +1 -1
  64. package/src/consumers/handlers/apiHandler.js +1 -1
  65. package/src/consumers/handlers/consoleHandler.js +1 -1
  66. package/src/consumers/handlers/databaseHandler.js +1 -1
  67. package/src/consumers/handlers/index.js +1 -1
  68. package/src/consumers/handlers/kafkaHandler.js +1 -1
  69. package/src/consumers/index.js +1 -1
  70. package/src/consumers/messageTransformer.js +1 -1
  71. package/src/consumers/validator.js +1 -1
  72. package/src/core/db/dialect/base-dialect.js +1 -1
  73. package/src/core/db/dialect/index.js +1 -1
  74. package/src/core/db/dialect/mysql-dialect.js +1 -1
  75. package/src/core/db/dialect/oracle-dialect.js +1 -1
  76. package/src/core/db/dialect/postgres-dialect.js +1 -1
  77. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  78. package/src/core/db/flatten-helper.js +1 -1
  79. package/src/core/db/query-builder-error.js +1 -1
  80. package/src/core/db/query-builder.js +1 -1
  81. package/src/core/db/relation-helper.js +1 -1
  82. package/src/core/handlers/delete_handler.js +1 -1
  83. package/src/core/handlers/insert_handler.js +1 -1
  84. package/src/core/handlers/update_handler.js +1 -1
  85. package/src/core/models/base-model.js +1 -1
  86. package/src/core/utils/cache-manager.js +1 -1
  87. package/src/core/utils/component-engine.js +1 -1
  88. package/src/core/utils/context-builder.js +1 -1
  89. package/src/core/utils/datetime-formatter.js +1 -1
  90. package/src/core/utils/datetime-parser.js +1 -1
  91. package/src/core/utils/db.js +1 -1
  92. package/src/core/utils/logger.js +1 -1
  93. package/src/core/utils/payload-loader.js +1 -1
  94. package/src/core/utils/security-checks.js +1 -1
  95. package/src/middleware/body-options.js +1 -1
  96. package/src/middleware/cors.js +1 -1
  97. package/src/middleware/idempotency.js +1 -1
  98. package/src/middleware/rate-limiter.js +1 -1
  99. package/src/middleware/request-logger.js +1 -1
  100. package/src/middleware/security-headers.js +1 -1
  101. package/src/models/base-model-mysql.js +1 -1
  102. package/src/models/base-model-oracle.js +1 -1
  103. package/src/models/base-model-sqlite.js +1 -1
  104. package/src/models/base-model.js +1 -1
  105. package/src/pro/caching/redis-client.js +1 -1
  106. package/src/pro/caching/redis-helper.js +1 -1
  107. package/src/pro/consumers/baseConsumer.js +1 -1
  108. package/src/pro/consumers/declarativeMapper.js +1 -1
  109. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  110. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  111. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  112. package/src/pro/consumers/handlers/index.js +1 -1
  113. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  114. package/src/pro/consumers/index.js +1 -1
  115. package/src/pro/consumers/messageTransformer.js +1 -1
  116. package/src/pro/consumers/validator.js +1 -1
  117. package/src/pro/database/base-model-mysql.js +1 -1
  118. package/src/pro/database/base-model-oracle.js +1 -1
  119. package/src/pro/database/base-model-sqlite.js +1 -1
  120. package/src/pro/database/db-mysql.js +1 -1
  121. package/src/pro/database/db-oracle.js +1 -1
  122. package/src/pro/database/db-sqlite.js +1 -1
  123. package/src/pro/excel/excel-generator.js +1 -1
  124. package/src/pro/excel/excel-parser.js +1 -1
  125. package/src/pro/excel/export-service.js +1 -1
  126. package/src/pro/excel/export_handler.js +1 -1
  127. package/src/pro/excel/import-service.js +1 -1
  128. package/src/pro/excel/import-validator.js +1 -1
  129. package/src/pro/excel/import_handler.js +1 -1
  130. package/src/pro/excel/upsert-builder.js +1 -1
  131. package/src/pro/idgen/idgen-routes.js +1 -1
  132. package/src/pro/integrations/lookup-resolver.js +1 -1
  133. package/src/pro/integrations/upload-handler-v2.js +1 -1
  134. package/src/pro/integrations/upload-handler.js +1 -1
  135. package/src/pro/integrations/webhook.js +1 -1
  136. package/src/pro/locking/lock-routes.js +1 -1
  137. package/src/pro/locking/resource-lock-manager.js +1 -1
  138. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  139. package/src/pro/messaging/kafkaService.js +1 -1
  140. package/src/pro/messaging/messagehubService.js +1 -1
  141. package/src/pro/messaging/rabbitmqService.js +1 -1
  142. package/src/pro/scheduler/job-manager.js +1 -1
  143. package/src/pro/scheduler/job-routes.js +1 -1
  144. package/src/pro/scheduler/job-validator.js +1 -1
  145. package/src/pro/storage/base-storage-provider.js +1 -1
  146. package/src/pro/storage/file-metadata-helper.js +1 -1
  147. package/src/pro/storage/index.js +1 -1
  148. package/src/pro/storage/local-storage-provider.js +1 -1
  149. package/src/pro/storage/s3-storage-provider.js +1 -1
  150. package/src/pro/storage/upload-cleanup-job.js +1 -1
  151. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  152. package/src/pro/storage/upload-pending-tracker.js +1 -1
  153. package/src/pro/websocket/broadcast-helper.js +1 -1
  154. package/src/pro/websocket/index.js +1 -1
  155. package/src/pro/websocket/livesync-server.js +1 -1
  156. package/src/pro/websocket/ws-broadcaster.js +1 -1
  157. package/src/services/export-service.js +1 -1
  158. package/src/services/import-service.js +1 -1
  159. package/src/services/kafkaConsumerService.js +1 -1
  160. package/src/services/kafkaService.js +1 -1
  161. package/src/services/messagehubService.js +1 -1
  162. package/src/services/rabbitmqService.js +1 -1
  163. package/src/utils/cache-invalidation-registry.js +1 -1
  164. package/src/utils/cache-manager.js +1 -1
  165. package/src/utils/component-engine.js +1 -1
  166. package/src/utils/config-extractor.js +1 -1
  167. package/src/utils/consumerLogger.js +1 -1
  168. package/src/utils/context-builder.js +1 -1
  169. package/src/utils/dashboard-helpers.js +1 -1
  170. package/src/utils/dateHelper.js +1 -1
  171. package/src/utils/datetime-formatter.js +1 -1
  172. package/src/utils/datetime-parser.js +1 -1
  173. package/src/utils/db-bootstrap.js +1 -1
  174. package/src/utils/db-mysql.js +1 -1
  175. package/src/utils/db-oracle.js +1 -1
  176. package/src/utils/db-sqlite.js +1 -1
  177. package/src/utils/db.js +1 -1
  178. package/src/utils/demo-generator.js +1 -1
  179. package/src/utils/excel-generator.js +1 -1
  180. package/src/utils/excel-parser.js +1 -1
  181. package/src/utils/file-watcher.js +1 -1
  182. package/src/utils/id-generator.js +1 -1
  183. package/src/utils/idempotency-manager.js +1 -1
  184. package/src/utils/import-validator.js +1 -1
  185. package/src/utils/license-client.js +1 -1
  186. package/src/utils/lock-manager.js +1 -1
  187. package/src/utils/logger.js +1 -1
  188. package/src/utils/lookup-resolver.js +1 -1
  189. package/src/utils/payload-loader.js +1 -1
  190. package/src/utils/processor-response.js +1 -1
  191. package/src/utils/rabbitmq.js +1 -1
  192. package/src/utils/redis-client.js +1 -1
  193. package/src/utils/redis-helper.js +1 -1
  194. package/src/utils/request-scope.js +1 -1
  195. package/src/utils/security-checks.js +1 -1
  196. package/src/utils/service-resolver.js +1 -1
  197. package/src/utils/shutdown-coordinator.js +1 -1
  198. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  199. package/src/utils/sql-table-extractor.js +1 -1
  200. package/src/utils/trusted-keys.js +1 -1
  201. package/src/utils/upload-handler.js +1 -1
  202. package/src/utils/upsert-builder.js +1 -1
  203. package/src/utils/workflow-hook-executor.js +1 -1
@@ -34,6 +34,8 @@ const DemoGenerator = require('../../src/utils/demo-generator');
34
34
  const projectRegistry = require('../../lib/utils/project-registry');
35
35
  const cliOutput = require('../../lib/utils/cli-output');
36
36
  const endpointSchemaValidator = require('../../lib/payload/endpoint-schema-validator');
37
+ const { deriveFieldProjections, augmentProjectionsForSoftDelete } = require('../../lib/payload/field-projections');
38
+ const configResolver = require('../../lib/utils/config-resolver');
37
39
 
38
40
  function hasAuditRequired(payload) {
39
41
  if (!payload || !payload.fieldPolicy) return false;
@@ -143,14 +145,12 @@ module.exports = {
143
145
  async handler(args) {
144
146
  const startTime = Date.now();
145
147
  let muted = false;
148
+ let summary = null;
146
149
 
147
150
  try {
148
151
  const project = ArgumentValidator.validateProjectName(args.project);
149
152
  const endpoint = ArgumentValidator.validateEndpointName(args.name);
150
153
  const payloadFile = ArgumentValidator.validatePayloadName(args.payload);
151
- const database = args.database
152
- ? ArgumentValidator.validateDatabaseType(args.database)
153
- : 'postgres';
154
154
  const force = !!args.force;
155
155
  const createExamples = !!args['create-examples'];
156
156
  const skipSqlValidation = !!args['skip-sql-validation'];
@@ -161,6 +161,30 @@ module.exports = {
161
161
  ? args.config.trim()
162
162
  : null;
163
163
 
164
+ // Resolusi tipe database:
165
+ // 1. --database eksplisit → dipakai apa adanya (prioritas tertinggi)
166
+ // 2. Auto-deteksi DB_TYPE dari config aktif (--config, atau default
167
+ // config .restforge/defaults.json) → mengikuti DB_TYPE project
168
+ // 3. Fallback 'postgres' bila tidak ada config yang bisa di-resolve
169
+ let database;
170
+ let databaseSource;
171
+ if (args.database) {
172
+ database = ArgumentValidator.validateDatabaseType(args.database);
173
+ databaseSource = 'flag';
174
+ } else {
175
+ const resolvedCfg = configResolver.resolveConfig(configArg, process.cwd());
176
+ const detected = resolvedCfg
177
+ ? configResolver.readDatabaseTypeFromConfig(resolvedCfg.path)
178
+ : null;
179
+ if (detected) {
180
+ database = detected;
181
+ databaseSource = resolvedCfg.source === 'default' ? 'config-default' : 'config';
182
+ } else {
183
+ database = 'postgres';
184
+ databaseSource = 'fallback';
185
+ }
186
+ }
187
+
164
188
  if (!verbose) {
165
189
  cliOutput.mute();
166
190
  muted = true;
@@ -170,12 +194,17 @@ module.exports = {
170
194
  console.log(` Project: ${project}`);
171
195
  console.log(` Endpoint: ${endpoint}`);
172
196
  console.log(` Payload: ${payloadFile}`);
173
- console.log(` Database: ${database}`);
197
+ const databaseNote = databaseSource === 'config'
198
+ ? ' (auto-detected from --config)'
199
+ : databaseSource === 'config-default'
200
+ ? ' (auto-detected from default config)'
201
+ : '';
202
+ console.log(` Database: ${database}${databaseNote}`);
174
203
  console.log(` Force overwrite: ${force}`);
175
204
  console.log('');
176
205
 
177
206
  const cwd = process.cwd();
178
- const summary = {
207
+ summary = {
179
208
  config: { project, endpoint, database, force },
180
209
  payload: null,
181
210
  archive: null,
@@ -204,7 +233,7 @@ module.exports = {
204
233
  const schemaResult = await endpointSchemaValidator.validateEndpointSchema({
205
234
  payload,
206
235
  payloadFileName: path.basename(payloadFile),
207
- payloadFilePath: payloadFile,
236
+ payloadFilePath: rawPayload._payloadPath,
208
237
  configArg,
209
238
  skipSchemaCheck,
210
239
  workingDir: cwd
@@ -216,6 +245,37 @@ module.exports = {
216
245
  summary.schemaValidation = schemaResult;
217
246
  summary.config.config = configArg || null;
218
247
 
248
+ // Derivasi field projections dan embed ke payload sebagai _fieldProjections.
249
+ // Template membaca ini untuk emit schemaFields/readableFields/datatablesFields.
250
+ // Fallback ke fieldName bila DB tidak tersedia (skipSchemaCheck atau projectionInputs absent).
251
+ {
252
+ const fn = payload.fieldName || [];
253
+ let fieldProjections;
254
+ if (schemaResult.status === 'ok' && schemaResult.projectionInputs) {
255
+ fieldProjections = deriveFieldProjections({
256
+ fieldName: fn,
257
+ physicalColumns: schemaResult.projectionInputs.physicalColumns,
258
+ readSourceColumns: schemaResult.projectionInputs.readSourceColumns,
259
+ datatablesColumns: schemaResult.projectionInputs.datatablesColumns,
260
+ overrides: {
261
+ readableFields: payload.readableFields,
262
+ datatablesFields: payload.datatablesFields
263
+ }
264
+ });
265
+ } else {
266
+ // skipSchemaCheck atau projectionInputs tidak tersedia:
267
+ // fallback langsung ke fieldName untuk semua proyeksi tanpa DB.
268
+ // Override payload.readableFields / datatablesFields diterapkan bila ada.
269
+ fieldProjections = {
270
+ schemaFields: fn.slice(),
271
+ readableFields: payload.readableFields || fn.slice(),
272
+ datatablesFields: payload.datatablesFields || fn.slice()
273
+ };
274
+ }
275
+ augmentProjectionsForSoftDelete(fieldProjections, payload);
276
+ payload._fieldProjections = fieldProjections;
277
+ }
278
+
219
279
  const registry = projectRegistry.loadProjectRegistry();
220
280
  if (registry.projects[project]) {
221
281
  const existing = registry.projects[project];
@@ -312,6 +372,9 @@ module.exports = {
312
372
  // cli-entry.js men-print `Error: <message>` ke stderr saat handler
313
373
  // re-throw (lihat handler dispatch). Jangan double-print di sini.
314
374
  if (muted) cliOutput.unmute();
375
+ if (summary && summary.config) {
376
+ cliOutput.printCreatePartial(summary);
377
+ }
315
378
  throw error;
316
379
  }
317
380
  }
@@ -259,7 +259,7 @@ function checkDesigner() {
259
259
 
260
260
  // Probe nyata: jalankan `restforge-designer --version`. shell:true memilih
261
261
  // cmd.exe (Windows) atau /bin/sh (Linux/macOS) otomatis, termasuk resolusi PATH.
262
- const r = spawnSync('restforge-designer --version', { shell: true, encoding: 'utf8' });
262
+ const r = spawnSync('npx restforge-designer --version', { shell: true, encoding: 'utf8' });
263
263
  const found = !r.error && r.status === 0;
264
264
  let version = '';
265
265
  if (found && r.stdout) version = r.stdout.trim().split(/\r?\n/)[0];
@@ -268,12 +268,12 @@ function checkDesigner() {
268
268
  console.log(leader(' > restforge-designer', 'NOT FOUND', 32));
269
269
  console.log('');
270
270
  console.log(` ${rule('=', 60)}`);
271
- console.log(' ERROR: restforge-designer is required but not installed.');
271
+ console.log(' ERROR: restforge-designer could not be started.');
272
272
  console.log(` ${rule('=', 60)}`);
273
- console.log(' fast-track generates the frontend app using restforge-designer.');
274
- console.log(' Download and install it from:');
273
+ console.log(' restforge-designer is bundled in @restforgejs/platform.');
274
+ console.log(' If this error appears, reinstall the package:');
275
275
  console.log('');
276
- console.log(' https://restforge.dev/download.html');
276
+ console.log(' npm install @restforgejs/platform');
277
277
  console.log('');
278
278
  console.log(' Then re-run this command.');
279
279
  console.log('');
@@ -657,7 +657,7 @@ const ANSI_RE = /\x1b\[[0-9;]*m/g;
657
657
  * (list hanya membaca metadata plugin built-in).
658
658
  */
659
659
  function runDesignerPluginsList() {
660
- const r = spawnSync('restforge-designer plugins list', { shell: true, encoding: 'utf8' });
660
+ const r = spawnSync('npx restforge-designer plugins list', { shell: true, encoding: 'utf8' });
661
661
  if (r.error || r.status !== 0) return null;
662
662
  return r.stdout || '';
663
663
  }
@@ -1251,7 +1251,7 @@ function runFrontendPipeline(ctx) {
1251
1251
  } catch {
1252
1252
  // File belum ada (first run) - abaikan.
1253
1253
  }
1254
- run(`restforge-designer generate --payload=payload/${appCode}.json --output=./apps/${ctx.project} ${pluginArg} --overwrite`, frontendDir);
1254
+ run(`npx restforge-designer generate --payload=payload/${appCode}.json --output=./apps/${ctx.project} ${pluginArg} --overwrite`, frontendDir);
1255
1255
  }
1256
1256
 
1257
1257
  /**
@@ -32,10 +32,11 @@ module.exports = {
32
32
  description: 'Sync only a specific table (default: all)'
33
33
  },
34
34
  'expand-fk': {
35
- type: 'boolean',
35
+ type: 'string',
36
36
  required: false,
37
- default: false,
38
- description: 'Generate JOIN configuration from foreign keys: creates SQL file query/<table>-join.sql and sets datatablesQuery/viewQuery to that file. Opt-in; without this flag sync behavior is unchanged. Requires --table. If --fk-columns is empty, display columns per FK are auto-selected (name → code → primary key)'
37
+ default: null,
38
+ bareDefault: 'both',
39
+ description: 'Generate JOIN configuration from foreign keys: creates SQL file query/<table>-join.sql. Values: "both" (updates datatablesQuery and viewQuery) or "datatables-only" (updates datatablesQuery only, viewQuery unchanged). Bare --expand-fk (without value) defaults to "both". Requires --table. If --fk-columns is empty, display columns per FK are auto-selected (name → code → primary key)'
39
40
  },
40
41
  'fk-columns': {
41
42
  type: 'string',
@@ -47,16 +48,25 @@ module.exports = {
47
48
  examples: [
48
49
  'npx restforge payload sync --config=db.env',
49
50
  'npx restforge payload sync --config=db.env --table=users',
50
- 'npx restforge payload sync --table=visitors --expand-fk',
51
- 'npx restforge payload sync --table=visitors --expand-fk --fk-columns=visitor_categories.category_code,visitor_categories.category_name'
51
+ 'npx restforge payload sync --table=visitors --expand-fk=both',
52
+ 'npx restforge payload sync --table=visitors --expand-fk=datatables-only',
53
+ 'npx restforge payload sync --table=visitors --expand-fk=both --fk-columns=visitor_categories.category_code,visitor_categories.category_name'
52
54
  ],
53
55
  async handler(args) {
56
+ const expandFkRaw = args['expand-fk'];
57
+ let expandFkMode = null;
58
+ if (expandFkRaw === 'both' || expandFkRaw === 'datatables-only') {
59
+ expandFkMode = expandFkRaw;
60
+ } else if (expandFkRaw !== null && expandFkRaw !== undefined) {
61
+ throw new Error(`Invalid --expand-fk value "${expandFkRaw}". Valid values: both, datatables-only`);
62
+ }
63
+
54
64
  const generator = new PayloadGenerator();
55
65
  await generator.run({
56
66
  config: args.config,
57
67
  table: args.table || null,
58
68
  sync: true,
59
- expandFk: args['expand-fk'] === true,
69
+ expandFkMode,
60
70
  fkColumns: args['fk-columns'] || null
61
71
  });
62
72
  }
@@ -7,8 +7,8 @@
7
7
  * RESTForge yang sudah ada. Setelah validasi prasyarat (phase 00), handler
8
8
  * menulis dua file SDF auth ber-prefix `rfx` ke `--schema-path` (Fungsi 1),
9
9
  * lalu membuat tabelnya di DB via primitif dbschema-kit langsung (Fungsi 2),
10
- * lalu menulis component middleware + router auth tanpa google (Fungsi 3a),
11
- * lalu menulis keenam processor auth tanpa google (Fungsi 3b), lalu
10
+ * lalu menulis component middleware + router auth termasuk route google (Fungsi 3a),
11
+ * lalu menulis ketujuh processor auth termasuk google (Fungsi 3b), lalu
12
12
  * menginjeksi variabel env auth ke `--config` dan memverifikasi/mencatat
13
13
  * dependency runtime `bcrypt`+`jsonwebtoken` (Fungsi tambahan, phase 05),
14
14
  * lalu mencetak ringkasan akhir.
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: project sdk
5
+ *
6
+ * Menghasilkan SDK JavaScript untuk satu project RESTForge — satu layer tipis di
7
+ * atas REST API hasil generate, dipakai frontend apa pun tanpa menulis ulang
8
+ * boilerplate fetch/$.ajax.
9
+ *
10
+ * Rujukan desain: docs/plan/sdk-generator-command.md.
11
+ *
12
+ * Versi awal:
13
+ * - TANPA auth (flag --with-auth & core/auth-client.js menyusul terpisah).
14
+ * - --generate hanya MENULIS source buildable (src/, package.json, tsup.config.js);
15
+ * `npm install && npm run build` adalah langkah terpisah milik user.
16
+ * - Sumber kebenaran resource = metadata/<project>.json (key endpoint = slug = segment route).
17
+ */
18
+
19
+ const { validateSafeName } = require('../../lib/utils/path-validator');
20
+ const { generateSdk, resolveBaseUrl } = require('../../lib/sdk/generator');
21
+
22
+ module.exports = {
23
+ resource: 'project',
24
+ verb: 'sdk',
25
+ description: 'Generate a JavaScript SDK for a project (derived from backend metadata + payload)',
26
+ category: 'generation',
27
+ flags: {
28
+ project: {
29
+ type: 'string',
30
+ required: true,
31
+ description: 'Target project name (also the SDK package name)'
32
+ },
33
+ generate: {
34
+ type: 'boolean',
35
+ required: true,
36
+ description: 'Trigger SDK source generation'
37
+ },
38
+ 'sdk-path': {
39
+ type: 'string',
40
+ required: false,
41
+ default: null,
42
+ description: 'Output folder for the SDK source (default: <project-root>/sdk)'
43
+ },
44
+ 'base-url': {
45
+ type: 'string',
46
+ required: false,
47
+ default: null,
48
+ description: 'Override the API base URL baked into sdk-client.js (default: derived from the project config)'
49
+ },
50
+ force: {
51
+ type: 'boolean',
52
+ required: false,
53
+ default: false,
54
+ description: 'Overwrite existing SDK source'
55
+ }
56
+ },
57
+ examples: [
58
+ 'npx restforge project sdk --generate --project=myapp',
59
+ 'npx restforge project sdk --generate --project=myapp --sdk-path=./client-sdk',
60
+ 'npx restforge project sdk --generate --project=myapp --force'
61
+ ],
62
+ async handler(args) {
63
+ const project = validateSafeName(args.project, 'project');
64
+
65
+ if (args.generate !== true) {
66
+ const err = new Error('The --generate flag must be set to run SDK generation.');
67
+ err.exitCode = 2;
68
+ throw err;
69
+ }
70
+
71
+ const workingDir = process.cwd();
72
+
73
+ console.log('');
74
+ console.log(`Generating SDK for project '${project}'...`);
75
+ console.log('');
76
+
77
+ const baseUrl = resolveBaseUrl({
78
+ workingDir,
79
+ project,
80
+ override: args['base-url'],
81
+ log: (line) => console.log(` ${line}`)
82
+ });
83
+
84
+ const result = generateSdk({
85
+ workingDir,
86
+ project,
87
+ sdkPath: args['sdk-path'],
88
+ baseUrl,
89
+ force: args.force === true,
90
+ log: (line) => console.log(` ${line}`)
91
+ });
92
+
93
+ console.log('');
94
+ console.log('==========================================');
95
+ console.log('SDK GENERATION COMPLETE');
96
+ console.log('==========================================');
97
+ console.log(`Project : ${project}`);
98
+ console.log(`Output : ${result.outputDir}`);
99
+ console.log(`Base URL : ${result.baseUrl}`);
100
+ console.log(`Auth : ${result.hasAuth ? 'enabled (client.auth)' : 'none'}`);
101
+ console.log(`Resources : ${result.resources.length} (${result.resources.join(', ')})`);
102
+ console.log('');
103
+ console.log('Next steps (optional, owned by you):');
104
+ console.log(` cd ${result.outputDir}`);
105
+ console.log(' npm install');
106
+ console.log(' npm run build');
107
+ console.log(' npm run deploy # copy into a frontend app js/ folder (asks for target)');
108
+ console.log(' # or: node deploy.mjs <target-app-js-folder>');
109
+ console.log('==========================================');
110
+ console.log('');
111
+ }
112
+ };
@@ -119,6 +119,12 @@ function parseArgs(argv, contract) {
119
119
 
120
120
  if (!valueProvided) {
121
121
  if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
122
+ if (flagDef.bareDefault !== undefined) {
123
+ args[flagName] = flagDef.bareDefault;
124
+ seenFlags.add(flagName);
125
+ i += 1;
126
+ continue;
127
+ }
122
128
  errors.push(`Flag --${flagName} requires a value`);
123
129
  i += 1;
124
130
  continue;
@@ -1,13 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Generator processor auth (Fungsi 3b). Merender keenam aset template di
4
+ * Generator processor auth (Fungsi 3b). Merender aset template di
5
5
  * `templates/processor/` via template-renderer (Phase 03), lalu menulis ke
6
6
  * lokasi target memakai writeFileWithBackup — perilaku force/skip identik
7
7
  * Fungsi 1/3a: tanpa --force file existing di-skip, dengan --force
8
8
  * di-overwrite + backup.
9
9
  *
10
- * `google.js` TIDAK disertakan (keputusan #5 campaign).
10
+ * `google.js` (Sign in with Google) disertakan; endpoint membutuhkan
11
+ * GOOGLE_CLIENT_ID di env (di-inject env-injector) dan ID token dari Google
12
+ * Identity Services di sisi frontend.
11
13
  */
12
14
 
13
15
  const fs = require('fs');
@@ -19,7 +21,7 @@ const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE, AUTH_MIDDLEWARE_NAME } = requ
19
21
 
20
22
  const TEMPLATES_DIR = path.join(__dirname, 'templates', 'processor');
21
23
 
22
- const PROCESSOR_NAMES = ['register', 'login', 'refresh', 'logout', 'me', 'reset-password'];
24
+ const PROCESSOR_NAMES = ['register', 'login', 'google', 'refresh', 'logout', 'me', 'reset-password'];
23
25
 
24
26
  const PARAMS = {
25
27
  AUTH_USER_TABLE,
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: google (Sign in with Google)
5
+ * Verifikasi Google ID token, lalu find-or-create user (identitas = email) dan
6
+ * terbitkan JWT access token + refresh token (sama seperti login biasa).
7
+ * Method: POST body: { credential: "<google-id-token>" }
8
+ *
9
+ * Verifikasi ID token memakai endpoint tokeninfo Google (tanpa dependensi npm
10
+ * tambahan). Untuk produksi, pertimbangkan google-auth-library (verifikasi JWKS
11
+ * lokal) agar tidak bergantung pada round-trip ke Google tiap request.
12
+ */
13
+
14
+ const bcrypt = require('bcrypt');
15
+ const crypto = require('crypto');
16
+ const {
17
+ generateAccessToken,
18
+ getAccessTokenExpiryMin
19
+ } = require('../../../../components/handlers/{{AUTH_MIDDLEWARE_NAME}}');
20
+
21
+ const BCRYPT_ROUNDS = 12;
22
+
23
+ function getRefreshExpiryDays() {
24
+ return parseInt(process.env.REFRESH_TOKEN_EXPIRY_DAYS || '30', 10);
25
+ }
26
+
27
+ async function verifyGoogleIdToken(credential) {
28
+ const url = 'https://oauth2.googleapis.com/tokeninfo?id_token=' + encodeURIComponent(credential);
29
+ const res = await fetch(url);
30
+ if (!res.ok) return null;
31
+ return res.json();
32
+ }
33
+
34
+ module.exports = {
35
+ async process(input, services, req) {
36
+ const { db, logger } = services;
37
+
38
+ try {
39
+ const { credential } = input;
40
+
41
+ if (!credential) {
42
+ return {
43
+ success: false,
44
+ statusCode: 400,
45
+ message: 'Field credential (Google ID token) is required.',
46
+ timestamp: new Date().toISOString()
47
+ };
48
+ }
49
+
50
+ const clientId = process.env.GOOGLE_CLIENT_ID;
51
+ if (!clientId) {
52
+ return {
53
+ success: false,
54
+ statusCode: 501,
55
+ message: 'Google login is not configured on the server (GOOGLE_CLIENT_ID is empty).',
56
+ timestamp: new Date().toISOString()
57
+ };
58
+ }
59
+
60
+ const info = await verifyGoogleIdToken(credential);
61
+ if (!info || !info.email) {
62
+ return {
63
+ success: false,
64
+ statusCode: 401,
65
+ message: 'Invalid Google token.',
66
+ timestamp: new Date().toISOString()
67
+ };
68
+ }
69
+
70
+ // Pastikan token memang ditujukan untuk aplikasi ini.
71
+ if (info.aud !== clientId) {
72
+ return {
73
+ success: false,
74
+ statusCode: 401,
75
+ message: 'Google token was not issued for this application.',
76
+ timestamp: new Date().toISOString()
77
+ };
78
+ }
79
+
80
+ // Google menandai email_verified sebagai string 'true'.
81
+ if (info.email_verified !== 'true' && info.email_verified !== true) {
82
+ return {
83
+ success: false,
84
+ statusCode: 403,
85
+ message: 'Google email is not verified.',
86
+ timestamp: new Date().toISOString()
87
+ };
88
+ }
89
+
90
+ const email = String(info.email).toLowerCase();
91
+ const fullName = info.name || null;
92
+
93
+ // Identitas = email. Satukan dengan akun manual ber-email sama.
94
+ const rows = await db.executeQuery(
95
+ `SELECT user_id, username, email, full_name, is_active, is_locked
96
+ FROM public.{{AUTH_USER_TABLE}} WHERE username = $1`,
97
+ [email]
98
+ );
99
+ let user = rows[0] || null;
100
+
101
+ if (user) {
102
+ if (!user.is_active || user.is_locked) {
103
+ return {
104
+ success: false,
105
+ statusCode: 403,
106
+ message: 'Account is inactive or locked.',
107
+ timestamp: new Date().toISOString()
108
+ };
109
+ }
110
+ await db.executeQuery(
111
+ `UPDATE public.{{AUTH_USER_TABLE}}
112
+ SET last_login_at = NOW(), failed_login_count = 0, updated_at = NOW()
113
+ WHERE user_id = $1`,
114
+ [user.user_id]
115
+ );
116
+ } else {
117
+ // User baru via Google: password acak (tidak dapat dipakai login manual
118
+ // sampai user reset password). password_hash tetap non-null sesuai schema.
119
+ const randomPw = crypto.randomBytes(32).toString('hex');
120
+ const passwordHash = await bcrypt.hash(randomPw, BCRYPT_ROUNDS);
121
+ const userId = crypto.randomUUID();
122
+
123
+ await db.executeQuery(
124
+ `INSERT INTO public.{{AUTH_USER_TABLE}}
125
+ (user_id, username, email, full_name, password_hash,
126
+ is_active, is_locked, failed_login_count,
127
+ last_login_at, password_changed_at, created_at)
128
+ VALUES ($1, $2, $3, $4, $5, TRUE, FALSE, 0, NOW(), NOW(), NOW())`,
129
+ [userId, email, email, fullName, passwordHash]
130
+ );
131
+
132
+ user = { user_id: userId, username: email, email: email, full_name: fullName };
133
+ }
134
+
135
+ const accessToken = generateAccessToken(user);
136
+ const expiresIn = getAccessTokenExpiryMin() * 60;
137
+
138
+ const refreshTokenRaw = `${user.user_id}:${crypto.randomBytes(64).toString('hex')}`;
139
+ const refreshTokenHash = await bcrypt.hash(refreshTokenRaw, BCRYPT_ROUNDS);
140
+ const refreshExpiresAt = new Date();
141
+ refreshExpiresAt.setDate(refreshExpiresAt.getDate() + getRefreshExpiryDays());
142
+
143
+ await db.executeQuery(
144
+ `INSERT INTO public.{{AUTH_REFRESH_TOKEN_TABLE}}
145
+ (token_id, user_id, token_hash, expires_at, created_at)
146
+ VALUES ($1, $2, $3, $4, NOW())`,
147
+ [crypto.randomUUID(), user.user_id, refreshTokenHash, refreshExpiresAt]
148
+ );
149
+
150
+ return {
151
+ success: true,
152
+ statusCode: 200,
153
+ message: 'Google login successful.',
154
+ data: {
155
+ access_token: accessToken,
156
+ refresh_token: refreshTokenRaw,
157
+ token_type: 'Bearer',
158
+ expires_in: expiresIn,
159
+ user: {
160
+ user_id: user.user_id,
161
+ username: user.username,
162
+ email: user.email,
163
+ full_name: user.full_name
164
+ }
165
+ },
166
+ timestamp: new Date().toISOString()
167
+ };
168
+ } catch (error) {
169
+ logger.error({ error: error.message }, '[auth-google] Unexpected error');
170
+ return {
171
+ success: false,
172
+ statusCode: 500,
173
+ message: 'An internal server error occurred.',
174
+ timestamp: new Date().toISOString()
175
+ };
176
+ }
177
+ }
178
+ };
@@ -31,7 +31,7 @@ module.exports = {
31
31
  return {
32
32
  success: false,
33
33
  statusCode: 400,
34
- message: 'Field username dan password wajib diisi.',
34
+ message: 'Username and password are required.',
35
35
  timestamp: new Date().toISOString()
36
36
  };
37
37
  }
@@ -49,7 +49,7 @@ module.exports = {
49
49
  return {
50
50
  success: false,
51
51
  statusCode: 401,
52
- message: 'Username atau password salah.',
52
+ message: 'Invalid username or password.',
53
53
  timestamp: new Date().toISOString()
54
54
  };
55
55
  }
@@ -58,7 +58,7 @@ module.exports = {
58
58
  return {
59
59
  success: false,
60
60
  statusCode: 403,
61
- message: 'Akun tidak aktif. Hubungi administrator.',
61
+ message: 'Account is inactive. Contact administrator.',
62
62
  timestamp: new Date().toISOString()
63
63
  };
64
64
  }
@@ -67,7 +67,7 @@ module.exports = {
67
67
  return {
68
68
  success: false,
69
69
  statusCode: 403,
70
- message: 'Akun terkunci karena terlalu banyak percobaan login gagal. Hubungi administrator.',
70
+ message: 'Account locked due to too many failed login attempts. Contact administrator.',
71
71
  timestamp: new Date().toISOString()
72
72
  };
73
73
  }
@@ -86,8 +86,8 @@ module.exports = {
86
86
  );
87
87
 
88
88
  const message = shouldLock
89
- ? 'Akun terkunci karena terlalu banyak percobaan login gagal. Hubungi administrator.'
90
- : `Username atau password salah. Sisa percobaan: ${MAX_FAILED_LOGIN - newFailedCount}.`;
89
+ ? 'Account locked due to too many failed login attempts. Contact administrator.'
90
+ : `Invalid username or password. Attempts remaining: ${MAX_FAILED_LOGIN - newFailedCount}.`;
91
91
 
92
92
  return {
93
93
  success: false,
@@ -124,7 +124,7 @@ module.exports = {
124
124
  return {
125
125
  success: true,
126
126
  statusCode: 200,
127
- message: 'Login berhasil.',
127
+ message: 'Login successful.',
128
128
  data: {
129
129
  access_token: accessToken,
130
130
  refresh_token: refreshTokenRaw,
@@ -144,7 +144,7 @@ module.exports = {
144
144
  return {
145
145
  success: false,
146
146
  statusCode: 500,
147
- message: 'Terjadi kesalahan internal pada server.',
147
+ message: 'An internal server error occurred.',
148
148
  timestamp: new Date().toISOString()
149
149
  };
150
150
  }
@@ -42,7 +42,7 @@ module.exports = {
42
42
  return {
43
43
  success: true,
44
44
  statusCode: 200,
45
- message: 'Logout berhasil.',
45
+ message: 'Logout successful.',
46
46
  timestamp: new Date().toISOString()
47
47
  };
48
48
  } catch (error) {
@@ -50,7 +50,7 @@ module.exports = {
50
50
  return {
51
51
  success: false,
52
52
  statusCode: 500,
53
- message: 'Terjadi kesalahan internal pada server.',
53
+ message: 'An internal server error occurred.',
54
54
  timestamp: new Date().toISOString()
55
55
  };
56
56
  }
@@ -39,7 +39,7 @@ module.exports = {
39
39
  return {
40
40
  success: false,
41
41
  statusCode: 404,
42
- message: 'User tidak ditemukan.',
42
+ message: 'User not found.',
43
43
  timestamp: new Date().toISOString()
44
44
  };
45
45
  }
@@ -56,7 +56,7 @@ module.exports = {
56
56
  return {
57
57
  success: false,
58
58
  statusCode: 500,
59
- message: 'Terjadi kesalahan internal pada server.',
59
+ message: 'An internal server error occurred.',
60
60
  timestamp: new Date().toISOString()
61
61
  };
62
62
  }