yaml-admin-api 0.0.7 → 0.0.8
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/README.md +243 -0
- package/package.json +1 -1
- package/src/yml-admin-api.js +19 -10
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
## yaml-admin-api
|
|
2
|
+
|
|
3
|
+
Build an admin API from a single YAML file. The library reads `admin.yml`, connects to MongoDB, and automatically registers login/auth and CRUD routes to your Express app. It also supports file upload (S3/local) and Excel import/export.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
```bash
|
|
9
|
+
npm i yaml-admin-api
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Required environment variables
|
|
13
|
+
- `MONGODB_URL`: MongoDB connection string
|
|
14
|
+
- `JWT_SECRET`: JWT signing key
|
|
15
|
+
|
|
16
|
+
Placeholders `${MONGODB_URL}` and `${JWT_SECRET}` inside YAML are resolved at runtime from the environment.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Quick Start (Express)
|
|
21
|
+
```js
|
|
22
|
+
const express = require('express');
|
|
23
|
+
const bodyParser = require('body-parser');
|
|
24
|
+
const cookieParser = require('cookie-parser');
|
|
25
|
+
const { registerRoutes } = require('yaml-admin-api');
|
|
26
|
+
|
|
27
|
+
(async () => {
|
|
28
|
+
const app = express();
|
|
29
|
+
app.use(cookieParser());
|
|
30
|
+
app.use(bodyParser.urlencoded({ extended: true, limit: '30mb' }));
|
|
31
|
+
app.use(bodyParser.json({ limit: '30mb' }));
|
|
32
|
+
|
|
33
|
+
// Provide admin.yml path or a YAML string
|
|
34
|
+
await registerRoutes(app, { yamlPath: './admin.yml' });
|
|
35
|
+
|
|
36
|
+
app.listen(6911, () => console.log('API listening on 6911'));
|
|
37
|
+
})();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Options
|
|
41
|
+
```js
|
|
42
|
+
await registerRoutes(app, {
|
|
43
|
+
yamlPath: './admin.yml', // or yamlString: '<yml text>'
|
|
44
|
+
password: {
|
|
45
|
+
// Override password hashing for fields with type: password (default: sha512)
|
|
46
|
+
encrypt: (plain) => require('crypto').createHash('sha512').update(plain).digest('hex')
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### admin.yml example
|
|
54
|
+
```yaml
|
|
55
|
+
login:
|
|
56
|
+
jwt-secret: ${JWT_SECRET}
|
|
57
|
+
id-password:
|
|
58
|
+
entity: admin
|
|
59
|
+
id-field: email
|
|
60
|
+
password-field: pass
|
|
61
|
+
password-encoding: bcrypt
|
|
62
|
+
bcrypt-salt: 10
|
|
63
|
+
|
|
64
|
+
api-host:
|
|
65
|
+
uri: http://localhost:6911
|
|
66
|
+
web-host:
|
|
67
|
+
uri: http://localhost:6900
|
|
68
|
+
|
|
69
|
+
database:
|
|
70
|
+
mongodb:
|
|
71
|
+
uri: ${MONGODB_URL}
|
|
72
|
+
|
|
73
|
+
upload:
|
|
74
|
+
# For S3
|
|
75
|
+
# s3:
|
|
76
|
+
# access_key_id: ${S3_ACCESS_KEY_ID}
|
|
77
|
+
# secret_access_key: ${S3_SECRET_ACCESS_KEY}
|
|
78
|
+
# region: ${S3_REGION}
|
|
79
|
+
# bucket: ${S3_BUCKET}
|
|
80
|
+
# bucket_private: ${S3_BUCKET_PRIVATE}
|
|
81
|
+
# base_url: http://localhost:6911/upload
|
|
82
|
+
# base_url_private: http://localhost:6911/upload_private
|
|
83
|
+
# For local
|
|
84
|
+
local:
|
|
85
|
+
path: ./upload
|
|
86
|
+
path_private: ./upload_private
|
|
87
|
+
base_url: http://localhost:6911
|
|
88
|
+
|
|
89
|
+
entity:
|
|
90
|
+
member:
|
|
91
|
+
category: 'User'
|
|
92
|
+
label: 'User'
|
|
93
|
+
fields:
|
|
94
|
+
- { name: email, type: string, required: true }
|
|
95
|
+
- { name: pass, type: password, required: true }
|
|
96
|
+
- { name: name, type: string, required: true }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key field rules
|
|
100
|
+
- Default key is `'_id'` (objectId).
|
|
101
|
+
- To use a custom key, mark the field with `key: true`.
|
|
102
|
+
- `type: integer` + `autogenerate: true` → auto-incrementing ID
|
|
103
|
+
- `type: string` + `autogenerate: true` → UUID
|
|
104
|
+
|
|
105
|
+
Search rules
|
|
106
|
+
- Only fields declared under `entity.<name>.crud.list.search` are searchable in the list API.
|
|
107
|
+
- When `exact: false`, a partial match (regex) is used; integers always compare exactly.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Auth and tokens
|
|
112
|
+
- A JWT is issued on successful login.
|
|
113
|
+
- All protected requests must include header `x-access-token: <JWT>` (query `?token=` and cookie are also accepted).
|
|
114
|
+
|
|
115
|
+
Login endpoints
|
|
116
|
+
```http
|
|
117
|
+
GET /member/login?email={email}&pass={pass}
|
|
118
|
+
POST /member/login { email, pass }
|
|
119
|
+
GET /member/islogin // token verification
|
|
120
|
+
GET /member/logout
|
|
121
|
+
```
|
|
122
|
+
Sample response
|
|
123
|
+
```json
|
|
124
|
+
{ "r": true, "token": "<JWT>", "member": { "id": "...", "email": "...", "name": "..." } }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Entity CRUD endpoints
|
|
130
|
+
All endpoints require authentication.
|
|
131
|
+
|
|
132
|
+
- List: `GET /<entity>`
|
|
133
|
+
- Paging: `_start`, `_end`
|
|
134
|
+
- Sort: `_sort`, `_order` (`ASC`|`DESC`)
|
|
135
|
+
- Field search: `?field=value` (see `crud.list.search` in the schema)
|
|
136
|
+
- Total count header: `X-Total-Count`
|
|
137
|
+
|
|
138
|
+
- Show: `GET /<entity>/:id`
|
|
139
|
+
- Create: `POST /<entity>`
|
|
140
|
+
- ID generation follows field definition (`key`, `type`, `autogenerate`).
|
|
141
|
+
- Response includes `id` (react-admin compatibility).
|
|
142
|
+
- Update: `PUT /<entity>/:id`
|
|
143
|
+
- Key field is not updated.
|
|
144
|
+
- Delete: `DELETE /<entity>/:id`
|
|
145
|
+
- Hard delete by default; customizable hook points exist.
|
|
146
|
+
|
|
147
|
+
Sensitive fields and media
|
|
148
|
+
- Fields with `type: password` are removed from responses and hashed on save.
|
|
149
|
+
- Fields with `type: image|mp4|file` include preview URLs in the response.
|
|
150
|
+
- Private files return secure, short-lived URLs.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### File upload
|
|
155
|
+
Depending on configuration, S3 or local upload APIs are enabled. All upload APIs require authentication.
|
|
156
|
+
|
|
157
|
+
Local upload
|
|
158
|
+
```http
|
|
159
|
+
PUT /api/local/media/upload?ext=jpg // public storage path
|
|
160
|
+
PUT /api/local/media/upload/secure?ext=jpg // private storage path
|
|
161
|
+
```
|
|
162
|
+
Response: `{ r: true, key }`
|
|
163
|
+
|
|
164
|
+
Secure download
|
|
165
|
+
- The server issues temporary URLs like `/local-secure-download?key=...&token=...`.
|
|
166
|
+
- The token is a short-lived (5 min) JWT; clients can use the link directly.
|
|
167
|
+
|
|
168
|
+
S3 upload (pre-signed)
|
|
169
|
+
```http
|
|
170
|
+
GET /api/media/url/put/:ext // public bucket
|
|
171
|
+
GET /api/media/url/secure/put/:ext // private bucket
|
|
172
|
+
POST /api/media/url/secure/init/:ext // multipart init (private)
|
|
173
|
+
POST /api/media/url/secure/part // request part URL
|
|
174
|
+
POST /api/media/url/secure/complete // complete multipart
|
|
175
|
+
POST /api/media/url/secure/abort // abort multipart
|
|
176
|
+
```
|
|
177
|
+
Response includes `upload_url`, storage `key`, etc.
|
|
178
|
+
|
|
179
|
+
Note: S3 usage requires `upload.s3.*` configuration.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Excel export / import
|
|
184
|
+
Declare in the schema to enable endpoints:
|
|
185
|
+
```yaml
|
|
186
|
+
entity:
|
|
187
|
+
member:
|
|
188
|
+
crud:
|
|
189
|
+
list:
|
|
190
|
+
export:
|
|
191
|
+
fields:
|
|
192
|
+
- { name: email }
|
|
193
|
+
- { name: name }
|
|
194
|
+
import:
|
|
195
|
+
fields:
|
|
196
|
+
- { name: email }
|
|
197
|
+
- { name: name }
|
|
198
|
+
upsert: true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Endpoints
|
|
202
|
+
```http
|
|
203
|
+
POST /excel/<entity>/export // with body { filter }
|
|
204
|
+
POST /excel/<entity>/import // with body { base64 } (Excel file as Base64)
|
|
205
|
+
```
|
|
206
|
+
Export response
|
|
207
|
+
```json
|
|
208
|
+
{ "r": true, "url": "<secure-download-url>" }
|
|
209
|
+
```
|
|
210
|
+
Import response
|
|
211
|
+
```json
|
|
212
|
+
{ "r": true, "msg": "Import success - <n> rows effected" }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Serverless (Lambda) example
|
|
218
|
+
Wrap with `serverless-http` to deploy easily. See `example/api1`.
|
|
219
|
+
```js
|
|
220
|
+
'use strict';
|
|
221
|
+
const serverless = require('serverless-http');
|
|
222
|
+
|
|
223
|
+
exports.handler = async (event, context) => {
|
|
224
|
+
const app = await require('./app');
|
|
225
|
+
const handler = serverless(app);
|
|
226
|
+
return await handler(event, context);
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The sample `serverless.yml` uses region `ap-northeast-2` and runtime `nodejs20.x`.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### FAQ
|
|
235
|
+
- Auth fails: Ensure you send `x-access-token` header. Obtain it from `/member/login`.
|
|
236
|
+
- Need CORS headers: Add an application-level CORS middleware as shown in the example app.
|
|
237
|
+
- Media field has no URL: Response adds preview URLs (`..._preview`). Private files return signed URLs.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### License
|
|
242
|
+
MIT
|
|
243
|
+
|
package/package.json
CHANGED
package/src/yml-admin-api.js
CHANGED
|
@@ -6,20 +6,32 @@ const { generateLoginApi } = require('./crud/login-api-generator');
|
|
|
6
6
|
const { withConfig } = require('./login/auth.js');
|
|
7
7
|
const { generateUploadApi } = require('./upload/upload-api-generator');
|
|
8
8
|
|
|
9
|
+
const changeEnv = (yamlString, env = {}) => {
|
|
10
|
+
if (!yamlString) return yamlString;
|
|
11
|
+
const mergedEnv = { ...process.env, ...env };
|
|
12
|
+
return yamlString.replace(/\$\{([A-Z0-9_\.\-]+)\}/g, (match, varName) => {
|
|
13
|
+
console.log('env', varName, mergedEnv[varName]);
|
|
14
|
+
const value = mergedEnv[varName];
|
|
15
|
+
return value !== undefined && value !== null ? String(value) : match;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
async function registerRoutes(app, options = {}) {
|
|
10
|
-
const { yamlPath, yamlString } = options;
|
|
20
|
+
const { yamlPath, yamlString, env } = options;
|
|
11
21
|
let yml;
|
|
12
22
|
if(yamlPath) {
|
|
13
|
-
yml = await readYml(yamlPath);
|
|
23
|
+
yml = await readYml(yamlPath, env);
|
|
14
24
|
} else if(yamlString) {
|
|
15
|
-
|
|
25
|
+
const replaced = changeEnv(yamlString, env);
|
|
26
|
+
yml = yaml.parse(replaced);
|
|
16
27
|
} else {
|
|
17
28
|
let yamlString = await fs.readFile('./admin.yml', 'utf8');
|
|
18
29
|
if(!yamlString) {
|
|
19
30
|
throw new Error('admin.yml is not found. yamlPath or yamlString is required.')
|
|
20
31
|
}
|
|
21
|
-
|
|
22
|
-
yamlString = yamlString
|
|
32
|
+
|
|
33
|
+
yamlString = changeEnv(yamlString, env);
|
|
34
|
+
|
|
23
35
|
yml = yaml.parse(yamlString);
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -63,13 +75,10 @@ async function registerRoutes(app, options = {}) {
|
|
|
63
75
|
})
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
async function readYml(path) {
|
|
78
|
+
async function readYml(path, env = {}) {
|
|
67
79
|
let yml = await fs.readFile(path, 'utf8');
|
|
68
|
-
yml = yml
|
|
69
|
-
yml = yml.replace('${MONGODB_URL}', process.env.MONGODB_URL);
|
|
70
|
-
|
|
80
|
+
yml = changeEnv(yml, env);
|
|
71
81
|
return yaml.parse(yml);
|
|
72
|
-
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
module.exports = registerRoutes;
|