@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.
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/endpoint/create.js +69 -6
- package/generators/cli/payload/sync.js +16 -6
- package/generators/cli/project/auth.js +2 -2
- package/generators/cli/project/sdk.js +112 -0
- package/generators/lib/arg-parser.js +6 -0
- package/generators/lib/auth/processor-generator.js +5 -3
- package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
- package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
- package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
- package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
- package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
- package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
- package/generators/lib/generators/model-generator.js +46 -59
- package/generators/lib/help-generator.js +41 -3
- package/generators/lib/payload/endpoint-schema-validator.js +8 -3
- package/generators/lib/payload/field-projections.js +116 -0
- package/generators/lib/payload/payload-runner.js +164 -48
- package/generators/lib/payload/schema-diff.js +108 -0
- package/generators/lib/sdk/generator.js +719 -0
- package/generators/lib/sdk/naming.js +48 -0
- package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
- package/generators/lib/sdk/runtime/auth-client.js +186 -0
- package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
- package/generators/lib/sdk/runtime/http-client.js +81 -0
- package/generators/lib/sdk/runtime/resource-client.js +59 -0
- package/generators/lib/sdk/runtime/storage.js +31 -0
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/generators/lib/utils/cli-output.js +40 -0
- package/generators/lib/utils/config-resolver.js +61 -0
- package/generators/lib/utils/database-introspector.js +28 -5
- package/integrity-manifest.json +18 -18
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/soft-delete-dashboard-guard.js +1 -1
- package/src/utils/sql-table-extractor.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- 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
|
+
}
|