@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,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Helper penamaan untuk generator SDK.
5
+ *
6
+ * Sumber slug = key endpoint di `metadata/<project>.json` (segment route nyata,
7
+ * lihat src/modules/<project>.js yang me-mount router dengan
8
+ * `/api/<project>/<basename-file>`). Dari slug itu diturunkan:
9
+ * - client key : camelCase (`guest-book` -> `guestBook`) — key di object createClient()
10
+ * - factory : PascalCase (`guest-book` -> `GuestBook`) — nama function createXxxResource
11
+ * - file/global : kebab/Pascal sesuai konteks
12
+ *
13
+ * Semua turunan deterministik: split pada batas `-`, `_`, spasi, dan batas camelCase.
14
+ */
15
+
16
+ function splitWords(input) {
17
+ return String(input == null ? '' : input)
18
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
19
+ .split(/[-_\s]+/)
20
+ .map((word) => word.trim())
21
+ .filter(Boolean);
22
+ }
23
+
24
+ function capitalize(word) {
25
+ if (!word) return '';
26
+ return word.charAt(0).toUpperCase() + word.slice(1);
27
+ }
28
+
29
+ function toKebab(input) {
30
+ return splitWords(input).map((word) => word.toLowerCase()).join('-');
31
+ }
32
+
33
+ function toCamel(input) {
34
+ const words = splitWords(input).map((word) => word.toLowerCase());
35
+ if (words.length === 0) return '';
36
+ return words[0] + words.slice(1).map(capitalize).join('');
37
+ }
38
+
39
+ function toPascal(input) {
40
+ return splitWords(input).map((word) => capitalize(word.toLowerCase())).join('');
41
+ }
42
+
43
+ module.exports = {
44
+ splitWords,
45
+ toKebab,
46
+ toCamel,
47
+ toPascal
48
+ };
@@ -0,0 +1,207 @@
1
+ # __PROJECT__ SDK
2
+
3
+ Auto-generated JavaScript SDK for the `__PROJECT__` RESTForge backend — a thin layer over the
4
+ REST API so you call `client.<resource>.<verb>(payload)` instead of hand-writing
5
+ `fetch` / `$.ajax`. Generated by `restforge project sdk`.
6
+
7
+ > Do not edit `src/` by hand. Regenerate with
8
+ > `restforge project sdk --generate --project=__PROJECT__ --force`.
9
+
10
+ ## Overview
11
+
12
+ - Base URL: `__BASEURL__`
13
+ - Global (IIFE / browser): `window.__GLOBAL__`
14
+ - Auth: __AUTH_OVERVIEW__
15
+
16
+ ## Resources
17
+
18
+ | Resource | Client accessor | Primary key | Methods |
19
+ |----------|-----------------|-------------|---------|
20
+ __RESOURCE_TABLE__
21
+
22
+ Method names follow the SDK mapping: `workflow` becomes `changeStatus`, enabling `lookup`
23
+ also adds `lookupDynamic` (GET search), and `export` / `import` are not generated.
24
+
25
+ ## Fields per Resource
26
+
27
+ Field lists are derived from each resource's payload (`fieldValidation`). Auto-generated
28
+ primary keys and audit columns are filled by the backend — you do not send them on `create`.
29
+ Other fields (including optional auto-filled ones) may be sent.
30
+
31
+ __RESOURCE_FIELDS__
32
+
33
+ ## Layout
34
+
35
+ ```
36
+ __PROJECT__/
37
+ ├── package.json
38
+ ├── tsup.config.js
39
+ ├── deploy.mjs # copy build into a frontend app js/ folder
40
+ ├── sdk-client.js # browser bootstrap (baseUrl baked in)
41
+ ├── README.md
42
+ └── src/
43
+ ├── core/
44
+ __CORE_TREE__
45
+ ├── resources/
46
+ __RESOURCE_TREE__
47
+ └── index.js
48
+ ```
49
+
50
+ ## Install & Build
51
+
52
+ ```bash
53
+ npm install
54
+ npm run build
55
+ ```
56
+
57
+ `npm run build` produces three formats in `dist/`:
58
+
59
+ - `dist/index.js` — ESM (bundlers, `<script type="module">`)
60
+ - `dist/index.cjs` — CommonJS (Node.js / SSR)
61
+ - `dist/index.global.js` — IIFE global (`window.__GLOBAL__`) for classic `<script>`
62
+
63
+ ## Deploy to a Frontend App (Vanilla)
64
+
65
+ ```bash
66
+ npm run deploy
67
+ # or: node deploy.mjs <path-to-app>/js
68
+ ```
69
+
70
+ Copies the build into `<app>/js/sdk/` and `sdk-client.js` into `<app>/js/sdk-client.js`.
71
+ Then add to the HTML page, before other classic scripts:
72
+
73
+ ```html
74
+ <script type="module" src="js/sdk-client.js"></script>
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ ### Vanilla JS (no bundler)
80
+
81
+ `sdk-client.js` sets up the global client (baseUrl already baked in):
82
+
83
+ ```js
84
+ // js/sdk-client.js (generated)
85
+ import { createClient } from './sdk/index.js';
86
+ window.__GLOBAL__ = createClient({ baseUrl: '__BASEURL__' });
87
+ ```
88
+
89
+ Then in each page script (classic `<script>`), wrap the resource you need in a `getResource()`
90
+ helper — the same convention used by generated pages — and call its methods with
91
+ `.then()` / `.catch()`:
92
+
93
+ ```js
94
+ // name getResource() after the resource this page uses
95
+ function getResource() {
96
+ return window.__GLOBAL__.__FIRST_KEY__;
97
+ }
98
+
99
+ // list (server-side, DataTables-compatible): pass the raw response to DataTables
100
+ getResource().datatables({ start: 0, length: 10, search: { value: '' }, sort_columns: [] })
101
+ .then(function (json) { /* json.data, json.recordsTotal, json.draw */ })
102
+ .catch(function (err) { console.error(err.message); });
103
+
104
+ // create (update is the same but the body must include the primary key)
105
+ getResource().create(__FIRST_CREATE_BODY__)
106
+ .then(function (response) { /* response.success, response.data */ })
107
+ .catch(function (err) { console.error(err.message); });
108
+
109
+ // fetch one by primary key — the record is response.data[0]
110
+ getResource().first({ where: [{ key: '__FIRST_PK__', value: '...' }] })
111
+ .then(function (response) {
112
+ if (response.success && response.data && response.data.length > 0) {
113
+ var item = response.data[0];
114
+ }
115
+ });
116
+
117
+ // delete by primary key
118
+ getResource().delete({ where: [{ key: '__FIRST_PK__', value: '...' }] })
119
+ .then(function (response) { /* reload list */ });
120
+ ```
121
+
122
+ ### Bundler (Vite, webpack, Next.js)
123
+
124
+ ```bash
125
+ npm install file:<path-to-this-folder>
126
+ ```
127
+
128
+ ```js
129
+ import { createClient } from '__PROJECT__';
130
+
131
+ const client = createClient({ baseUrl: '__BASEURL__' });
132
+ const list = await client.__FIRST_KEY__.datatables({ start: 0, length: 10 });
133
+ ```
134
+
135
+ ### Error Handling
136
+
137
+ ```js
138
+ import { ApiError } from '__PROJECT__';
139
+
140
+ try {
141
+ await client.__FIRST_KEY__.create(__FIRST_CREATE_BODY__);
142
+ } catch (err) {
143
+ if (err instanceof ApiError) {
144
+ console.log(err.status, err.body); // HTTP status + backend response body
145
+ }
146
+ }
147
+ ```
148
+
149
+ __AUTH_SECTION__## Request & Response Conventions
150
+
151
+ All methods are POST with a JSON body, except `lookupDynamic` which is GET. The SDK sets
152
+ `Content-Type: application/json` and the `x-request-mode` header automatically; you only pass
153
+ the body shown below.
154
+
155
+ ### Response envelope
156
+
157
+ Most endpoints return:
158
+
159
+ ```json
160
+ { "success": true, "data": ... , "message": "..." }
161
+ ```
162
+
163
+ - `create` / `update`: `data` is the affected record (object).
164
+ - `first` / `read` / `lookup`: `data` is an **array** of records — a single record is `data[0]`.
165
+ - `datatables`: returns DataTables shape directly (`data`, `recordsTotal`, `recordsFiltered`, `draw`); `read` (paginated) adds `pagination`.
166
+
167
+ On failure the SDK throws `ApiError` with `.message`, `.status` (HTTP code), and `.body`
168
+ (the parsed error response).
169
+
170
+ ### Body shape per method
171
+
172
+ | Method | Body |
173
+ |--------|------|
174
+ | `create` | object of writable fields (see Fields per Resource) |
175
+ | `update` | writable fields **plus the primary key** |
176
+ | `delete` | `{ where: [{ key, value }] }` |
177
+ | `first` | `{ where: [{ key, value }], select?: string[] }` (also accepts a single `{ key, value }` object); returns one record |
178
+ | `read` | `{ page, per_page }` (paginated) or `{ limit }` (non-paginated); optional `search_value`, `search_by`, `sort_columns`, `where`, `select` |
179
+ | `datatables` | `{ start, length, search, searchBy, filters, sort_columns, where }` |
180
+ | `lookup` | `{ where?, select?, sort_columns? }` (static lookup) |
181
+ | `lookupDynamic` | `{ search }` (sent as query string) |
182
+ | `changeStatus` | `{ <primaryKey>, status, ... }` (workflow transition) |
183
+
184
+ ### `where` formats
185
+
186
+ ```js
187
+ // equality, array form (delete, first, read, datatables, lookup)
188
+ where: [{ key: '__FIRST_PK__', value: '...' }]
189
+
190
+ // first also accepts a single object
191
+ where: { key: '__FIRST_PK__', value: '...' }
192
+
193
+ // advanced (read / datatables / lookup)
194
+ where: {
195
+ logic: 'OR',
196
+ conditions: [
197
+ { key: '__FIRST_PK__', operator: 'like', value: '%abc%' },
198
+ { key: '__FIRST_PK__', value: 'exact' }
199
+ ]
200
+ }
201
+ ```
202
+
203
+ ### Sorting
204
+
205
+ ```js
206
+ sort_columns: [{ column: '__FIRST_PK__', direction: 'ASC' }]
207
+ ```
@@ -0,0 +1,186 @@
1
+ // Login/logout/refresh token + sesi user untuk auth extension RESTForge (`project auth`).
2
+ //
3
+ // Endpoint mengikuti router rfx_auth yang dipasang `project auth`, di-mount di bawah baseUrl
4
+ // resource yang sama: {baseUrl}/rfx_auth/login | /refresh | /logout | /me | /register | /reset-password.
5
+ // Tidak memakai app_code (itu khusus auth eksternal/multi-tenant, bukan project auth self-hosted).
6
+ //
7
+ // Response backend dibungkus { success, data: {...} }; token diambil dari data.access_token /
8
+ // data.refresh_token, user dari data.user (login) atau data (me).
9
+
10
+ import { ApiError } from './http-client.js';
11
+
12
+ const STORAGE_KEYS = {
13
+ accessToken: 'auth_access_token',
14
+ refreshToken: 'auth_refresh_token',
15
+ user: 'auth_user'
16
+ };
17
+
18
+ export function createAuthClient({ baseUrl, storage }) {
19
+ if (!baseUrl) throw new Error('createAuthClient: baseUrl wajib diisi');
20
+ if (!storage) throw new Error('createAuthClient: storage wajib diisi');
21
+
22
+ let refreshPromise = null;
23
+
24
+ function getAccessToken() {
25
+ return storage.get(STORAGE_KEYS.accessToken);
26
+ }
27
+
28
+ function getRefreshToken() {
29
+ return storage.get(STORAGE_KEYS.refreshToken);
30
+ }
31
+
32
+ function getCurrentUser() {
33
+ const raw = storage.get(STORAGE_KEYS.user);
34
+ if (!raw) return null;
35
+ try {
36
+ return JSON.parse(raw);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function isAuthenticated() {
43
+ const token = getAccessToken();
44
+ return !!token && !isTokenExpired(token);
45
+ }
46
+
47
+ function hasPermission(permissionName) {
48
+ const user = getCurrentUser();
49
+ if (!user || !Array.isArray(user.permissions)) return false;
50
+ return user.permissions.some((p) => (typeof p === 'string' ? p === permissionName : p.name === permissionName || p.code === permissionName));
51
+ }
52
+
53
+ function hasRole(roleName) {
54
+ const user = getCurrentUser();
55
+ if (!user || !Array.isArray(user.roles)) return false;
56
+ return user.roles.some((r) => (typeof r === 'string' ? r === roleName : r.name === roleName || r.code === roleName));
57
+ }
58
+
59
+ async function login(username, password) {
60
+ const data = await requestJson('POST', '/login', { username, password });
61
+ storeSession(data);
62
+ return getCurrentUser();
63
+ }
64
+
65
+ async function google(credential) {
66
+ // credential: Google ID token (JWT) dari Google Identity Services.
67
+ // Backend memverifikasi token, find-or-create user, lalu menerbitkan
68
+ // access/refresh token sama seperti login biasa.
69
+ const data = await requestJson('POST', '/google', { credential });
70
+ storeSession(data);
71
+ return getCurrentUser();
72
+ }
73
+
74
+ async function register(payload) {
75
+ // payload: { username, password, email?, full_name? }
76
+ return requestJson('POST', '/register', payload);
77
+ }
78
+
79
+ async function resetPassword(payload) {
80
+ // payload: { email, new_password, confirm_password }
81
+ return requestJson('POST', '/reset-password', payload);
82
+ }
83
+
84
+ function logout() {
85
+ const refreshToken = getRefreshToken();
86
+ const accessToken = getAccessToken();
87
+ clearSession();
88
+
89
+ // Fire-and-forget: invalidasi refresh token di server, tidak menunggu hasilnya.
90
+ fetch(baseUrl + '/logout', {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
95
+ },
96
+ body: JSON.stringify(refreshToken ? { refresh_token: refreshToken } : {})
97
+ }).catch(() => {});
98
+ }
99
+
100
+ async function refresh() {
101
+ if (refreshPromise) return refreshPromise;
102
+ refreshPromise = doRefresh();
103
+ try {
104
+ return await refreshPromise;
105
+ } finally {
106
+ refreshPromise = null;
107
+ }
108
+ }
109
+
110
+ async function doRefresh() {
111
+ const refreshToken = getRefreshToken();
112
+ if (!refreshToken) {
113
+ throw new Error('Refresh token tidak tersedia');
114
+ }
115
+ const data = await requestJson('POST', '/refresh', { refresh_token: refreshToken });
116
+ storeSession(data);
117
+ return data;
118
+ }
119
+
120
+ async function getMe() {
121
+ const accessToken = getAccessToken();
122
+ const response = await fetch(baseUrl + '/me', {
123
+ headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
124
+ });
125
+ const result = await response.json().catch(() => null);
126
+ if (!response.ok) {
127
+ throw new ApiError(result?.message || 'Failed to fetch profile data', response.status, result);
128
+ }
129
+ const userData = result?.data ?? result;
130
+ storage.set(STORAGE_KEYS.user, JSON.stringify(userData));
131
+ return userData;
132
+ }
133
+
134
+ async function requestJson(method, path, body) {
135
+ const response = await fetch(baseUrl + path, {
136
+ method,
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify(body)
139
+ });
140
+ const result = await response.json().catch(() => null);
141
+ if (!response.ok) {
142
+ throw new ApiError(result?.message || 'Request failed', response.status, result);
143
+ }
144
+ return result?.data ?? result;
145
+ }
146
+
147
+ function storeSession(data) {
148
+ if (!data) return;
149
+ if (data.access_token) storage.set(STORAGE_KEYS.accessToken, data.access_token);
150
+ if (data.refresh_token) storage.set(STORAGE_KEYS.refreshToken, data.refresh_token);
151
+ if (data.user) storage.set(STORAGE_KEYS.user, JSON.stringify(data.user));
152
+ }
153
+
154
+ function clearSession() {
155
+ storage.remove(STORAGE_KEYS.accessToken);
156
+ storage.remove(STORAGE_KEYS.refreshToken);
157
+ storage.remove(STORAGE_KEYS.user);
158
+ }
159
+
160
+ function isTokenExpired(token) {
161
+ try {
162
+ const payload = token.split('.')[1];
163
+ const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
164
+ if (!decoded.exp) return false;
165
+ return decoded.exp < Math.floor(Date.now() / 1000) + 30;
166
+ } catch {
167
+ return true;
168
+ }
169
+ }
170
+
171
+ return {
172
+ login,
173
+ google,
174
+ register,
175
+ resetPassword,
176
+ logout,
177
+ refresh,
178
+ getMe,
179
+ getAccessToken,
180
+ getRefreshToken,
181
+ getCurrentUser,
182
+ isAuthenticated,
183
+ hasPermission,
184
+ hasRole
185
+ };
186
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deploy the __PROJECT__ SDK into a frontend app's js/ folder.
4
+ *
5
+ * Run from inside this sdk/ folder:
6
+ * node deploy.mjs [path-to-app-js-folder] or npm run deploy
7
+ *
8
+ * What it does (all interaction is contained in this file):
9
+ * 1. Resolves the target app js/ folder (from the argument, or asks for it).
10
+ * 2. Builds dist/ automatically when it is missing (runs `npm run build`).
11
+ * 3. Copies the built files into <js>/sdk/ (index.js, index.cjs, index.global.js).
12
+ * 4. Copies sdk-client.js into <js>/sdk-client.js (skipped if it already exists).
13
+ * 5. Prints the <script> tag to add to the HTML page.
14
+ *
15
+ * No external dependencies — Node built-ins only.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import readline from 'node:readline';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { spawnSync } from 'node:child_process';
22
+
23
+ const SDK_DIR = path.dirname(fileURLToPath(import.meta.url));
24
+ const PROJECT = '__PROJECT__';
25
+ const DIST_DIR = path.join(SDK_DIR, 'dist');
26
+ const CLIENT_FILE = path.join(SDK_DIR, 'sdk-client.js');
27
+
28
+ function ask(question) {
29
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
+ return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }));
31
+ }
32
+
33
+ function fail(message) {
34
+ console.error(`\nDeploy failed: ${message}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ async function main() {
39
+ // 1. Target app js/ folder: from argv, or ask.
40
+ let target = process.argv[2];
41
+ if (!target) {
42
+ target = (await ask('Target app js/ folder path: ')).trim();
43
+ }
44
+ if (!target) fail('No target js/ folder provided.');
45
+ target = path.resolve(target);
46
+ if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
47
+ fail(`Target folder not found: ${target}`);
48
+ }
49
+
50
+ // 2. Ensure dist/ exists; build automatically when missing.
51
+ if (!fs.existsSync(path.join(DIST_DIR, 'index.js'))) {
52
+ console.log('dist/ not found — running "npm run build"...');
53
+ const built = spawnSync('npm', ['run', 'build'], { cwd: SDK_DIR, stdio: 'inherit', shell: true });
54
+ if (built.status !== 0) fail('"npm run build" failed. Run "npm install" first if needed.');
55
+ }
56
+
57
+ // 3. Copy built files into <js>/sdk/.
58
+ const destSdk = path.join(target, 'sdk');
59
+ fs.mkdirSync(destSdk, { recursive: true });
60
+ for (const file of fs.readdirSync(DIST_DIR)) {
61
+ fs.copyFileSync(path.join(DIST_DIR, file), path.join(destSdk, file));
62
+ }
63
+
64
+ // 4. Copy sdk-client.js into <js>/sdk-client.js (do not clobber a customized one).
65
+ const destClient = path.join(target, 'sdk-client.js');
66
+ let clientWritten = false;
67
+ if (fs.existsSync(CLIENT_FILE)) {
68
+ if (fs.existsSync(destClient)) {
69
+ console.log('sdk-client.js already exists in target — kept as is (delete it to regenerate).');
70
+ } else {
71
+ fs.copyFileSync(CLIENT_FILE, destClient);
72
+ clientWritten = true;
73
+ }
74
+ }
75
+
76
+ // 5. Instructions.
77
+ console.log(`\nSDK '${PROJECT}' deployed into: ${target}`);
78
+ console.log(` sdk/ (built client: index.js, index.cjs, index.global.js)`);
79
+ console.log(` sdk-client.js (${clientWritten ? 'written' : 'kept existing / not generated'})`);
80
+ console.log('\nAdd this to the HTML page, BEFORE other classic scripts:');
81
+ console.log(' <script type="module" src="js/sdk-client.js"></script>');
82
+ console.log('\nThen use window.<project> in your page scripts. Adjust baseUrl inside sdk-client.js if needed.');
83
+ }
84
+
85
+ main().catch((err) => fail(err && err.message ? err.message : String(err)));
@@ -0,0 +1,81 @@
1
+ // Wrapper fetch generik: base URL, header auth, retry sekali saat 401, normalize response/error.
2
+ // Tidak tahu apa pun soal login/storage — hanya menerima callback getAccessToken/refreshAccessToken
3
+ // yang di-inject dari luar (lihat core/auth-client.js dan src/index.js).
4
+
5
+ export class ApiError extends Error {
6
+ constructor(message, status, body) {
7
+ super(message);
8
+ this.name = 'ApiError';
9
+ this.status = status;
10
+ this.body = body;
11
+ }
12
+ }
13
+
14
+ export class HttpClient {
15
+ constructor({ baseUrl, getAccessToken, refreshAccessToken, onSessionExpired } = {}) {
16
+ if (!baseUrl) {
17
+ throw new Error('HttpClient: baseUrl wajib diisi');
18
+ }
19
+
20
+ this.baseUrl = baseUrl;
21
+ this.getAccessToken = getAccessToken || (() => null);
22
+ this.refreshAccessToken = refreshAccessToken || (() => Promise.reject(new Error('refreshAccessToken tidak tersedia')));
23
+ this.onSessionExpired = onSessionExpired || (() => {});
24
+ }
25
+
26
+ post(path, body = {}, options = {}) {
27
+ return this._request('POST', path, body, options);
28
+ }
29
+
30
+ get(path, options = {}) {
31
+ return this._request('GET', path, undefined, options);
32
+ }
33
+
34
+ async _request(method, path, body, options = {}, isRetry = false) {
35
+ const headers = { ...(options.headers || {}) };
36
+
37
+ const token = this.getAccessToken();
38
+ if (token) {
39
+ headers['Authorization'] = `Bearer ${token}`;
40
+ }
41
+
42
+ const isFormData = typeof FormData !== 'undefined' && body instanceof FormData;
43
+ if (method !== 'GET' && !isFormData && !headers['Content-Type']) {
44
+ headers['Content-Type'] = 'application/json';
45
+ }
46
+
47
+ const response = await fetch(this.baseUrl + path, {
48
+ method,
49
+ headers,
50
+ body: method === 'GET' ? undefined : (isFormData ? body : JSON.stringify(body))
51
+ });
52
+
53
+ if (response.status === 401 && !isRetry) {
54
+ try {
55
+ await this.refreshAccessToken();
56
+ } catch {
57
+ this.onSessionExpired();
58
+ throw new ApiError('Session has expired. Please log in again.', 401, null);
59
+ }
60
+ return this._request(method, path, body, options, true);
61
+ }
62
+
63
+ const result = await this._parseJson(response);
64
+
65
+ if (!response.ok) {
66
+ throw new ApiError(result?.message || `Request failed with status ${response.status}`, response.status, result);
67
+ }
68
+
69
+ return result;
70
+ }
71
+
72
+ async _parseJson(response) {
73
+ const text = await response.text();
74
+ if (!text) return null;
75
+ try {
76
+ return JSON.parse(text);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,59 @@
1
+ // Generic builder verb CRUD per resource.
2
+ // Descriptor (slug, primaryKey, action) berasal 1:1 dari payload RDF backend
3
+ // (mis. backend/payload/category.json) — generator cukup salin field tableName-slug,
4
+ // primaryKey, dan action ke sini, tanpa transformasi tambahan.
5
+
6
+ // Verb yang nama method-nya tidak sama dengan nama path REST (camelCase action key
7
+ // tidak selalu sama dengan kebab-case path).
8
+ const VERB_OVERRIDES = {
9
+ workflow: { method: 'changeStatus', path: 'change-status' }
10
+ };
11
+
12
+ // export & import sengaja TIDAK di-generate otomatis di sini: alurnya multi-step
13
+ // (upload/preview/commit/status/download), didesain terpisah saat resource yang
14
+ // memakainya sudah dibutuhkan di pilot ini.
15
+ const SKIP_VERBS = ['export', 'import'];
16
+
17
+ // lookup selalu dipanggil SDK lewat POST (mode "static" backend) — endpoint juga
18
+ // punya mode GET ("dynamic", untuk search-as-you-type), tapi itu di luar scope SDK
19
+ // generic ini. Header ini wajib supaya backend tidak salah baca mode.
20
+ const VERB_REQUEST_OPTIONS = {
21
+ lookup: { headers: { 'x-request-mode': 'static' } }
22
+ };
23
+
24
+ function camelToKebab(text) {
25
+ return text.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
26
+ }
27
+
28
+ /**
29
+ * @param {object} http - instance dari core/http-client.js, kontrak: http.post(path, body, options) => Promise<json>
30
+ * @param {object} descriptor - { slug, primaryKey, action }
31
+ */
32
+ export function createResource(http, descriptor) {
33
+ const { slug, primaryKey, action = {} } = descriptor;
34
+ const resource = { slug, primaryKey };
35
+
36
+ for (const [verb, isActive] of Object.entries(action)) {
37
+ if (!isActive || SKIP_VERBS.includes(verb)) continue;
38
+
39
+ const override = VERB_OVERRIDES[verb];
40
+ const method = override ? override.method : verb;
41
+ const path = override ? override.path : camelToKebab(verb);
42
+ const requestOptions = VERB_REQUEST_OPTIONS[verb];
43
+
44
+ resource[method] = (body = {}) => http.post(`/${slug}/${path}`, body, requestOptions);
45
+ }
46
+
47
+ // lookupDynamic: varian GET dari endpoint /lookup yang sama, untuk search-as-you-type
48
+ // (mis. select2 remote ajax) — backend MEMANG memfilter via query `search` hanya di mode
49
+ // ini, mode POST static di atas tidak punya parameter search sama sekali.
50
+ if (action.lookup) {
51
+ resource.lookupDynamic = (params = {}) => {
52
+ const query = new URLSearchParams(params).toString();
53
+ const path = `/${slug}/lookup${query ? `?${query}` : ''}`;
54
+ return http.get(path, { headers: { 'x-request-mode': 'dynamic' } });
55
+ };
56
+ }
57
+
58
+ return resource;
59
+ }