@hotfusion/modeller 0.0.13 → 0.0.15
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 +99 -0
- package/dist/adapters/cipher.js +51 -0
- package/dist/adapters/cipher.js.map +1 -0
- package/dist/connector.js +81 -41
- package/dist/connector.js.map +1 -1
- package/dist/core.js +2 -48
- package/dist/core.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/model.js +27 -50
- package/dist/model.js.map +1 -1
- package/dist/oidc/adapter.js +177 -0
- package/dist/oidc/adapter.js.map +1 -0
- package/dist/oidc/adapters/cipher.js +51 -0
- package/dist/oidc/adapters/cipher.js.map +1 -0
- package/dist/oidc/client.js +66 -0
- package/dist/oidc/client.js.map +1 -0
- package/dist/oidc/code.js +37 -0
- package/dist/oidc/code.js.map +1 -0
- package/dist/oidc/default.config.js +200 -0
- package/dist/oidc/default.config.js.map +1 -0
- package/dist/oidc/federation.js +51 -0
- package/dist/oidc/federation.js.map +1 -0
- package/dist/oidc/grant.js +37 -0
- package/dist/oidc/grant.js.map +1 -0
- package/dist/oidc/interaction.js +36 -0
- package/dist/oidc/interaction.js.map +1 -0
- package/dist/oidc/oidc.config.js +79 -0
- package/dist/oidc/oidc.config.js.map +1 -0
- package/dist/oidc/schemas/client.schema.json +62 -0
- package/dist/oidc/schemas/code.schema.json +16 -0
- package/dist/oidc/schemas/grant.schema.json +13 -0
- package/dist/oidc/schemas/interaction.schema.json +26 -0
- package/dist/oidc/schemas/session.schema.json +14 -0
- package/dist/oidc/schemas/token.schema.json +16 -0
- package/dist/oidc/schemas/user.schema.json +44 -0
- package/dist/oidc/session.js +36 -0
- package/dist/oidc/session.js.map +1 -0
- package/dist/oidc/session.token.js +24 -0
- package/dist/oidc/session.token.js.map +1 -0
- package/dist/oidc/token.js +23 -0
- package/dist/oidc/token.js.map +1 -0
- package/dist/oidc/user.js +95 -0
- package/dist/oidc/user.js.map +1 -0
- package/dist/oidc/utils.js +154 -0
- package/dist/oidc/utils.js.map +1 -0
- package/dist/server.js +722 -113
- package/dist/server.js.map +1 -1
- package/dist/types/adapters/cipher.d.ts +12 -0
- package/dist/types/adapters/cipher.d.ts.map +1 -0
- package/dist/types/connector.d.ts +13 -1
- package/dist/types/connector.d.ts.map +1 -1
- package/dist/types/core.d.ts +2 -2
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model.d.ts +26 -2
- package/dist/types/model.d.ts.map +1 -1
- package/dist/types/oidc/adapter.d.ts +16 -0
- package/dist/types/oidc/adapter.d.ts.map +1 -0
- package/dist/types/oidc/adapters/cipher.d.ts +12 -0
- package/dist/types/oidc/adapters/cipher.d.ts.map +1 -0
- package/dist/types/oidc/client.d.ts +3 -0
- package/dist/types/oidc/client.d.ts.map +1 -0
- package/dist/types/oidc/code.d.ts +3 -0
- package/dist/types/oidc/code.d.ts.map +1 -0
- package/dist/types/oidc/default.config.d.ts +33 -0
- package/dist/types/oidc/default.config.d.ts.map +1 -0
- package/dist/types/oidc/federation.d.ts +3 -0
- package/dist/types/oidc/federation.d.ts.map +1 -0
- package/dist/types/oidc/grant.d.ts +3 -0
- package/dist/types/oidc/grant.d.ts.map +1 -0
- package/dist/types/oidc/interaction.d.ts +3 -0
- package/dist/types/oidc/interaction.d.ts.map +1 -0
- package/dist/types/oidc/oidc.config.d.ts +7 -0
- package/dist/types/oidc/oidc.config.d.ts.map +1 -0
- package/dist/types/oidc/session.d.ts +3 -0
- package/dist/types/oidc/session.d.ts.map +1 -0
- package/dist/types/oidc/session.token.d.ts +3 -0
- package/dist/types/oidc/session.token.d.ts.map +1 -0
- package/dist/types/oidc/token.d.ts +3 -0
- package/dist/types/oidc/token.d.ts.map +1 -0
- package/dist/types/oidc/user.d.ts +3 -0
- package/dist/types/oidc/user.d.ts.map +1 -0
- package/dist/types/oidc/utils.d.ts +56 -0
- package/dist/types/oidc/utils.d.ts.map +1 -0
- package/dist/types/server.d.ts +8 -3
- package/dist/types/server.d.ts.map +1 -1
- package/dist/types/types.d.ts +264 -0
- package/dist/types/utils/bundler.d.ts.map +1 -1
- package/dist/types/utils/display.d.ts +23 -0
- package/dist/types/utils/display.d.ts.map +1 -0
- package/dist/utils/_secret.key +1 -0
- package/dist/utils/bundler.js +47 -8
- package/dist/utils/bundler.js.map +1 -1
- package/dist/utils/display.js +207 -0
- package/dist/utils/display.js.map +1 -0
- package/package.json +28 -4
- package/docs/CORE.md +0 -191
- package/docs/ERRORS.md +0 -90
- package/docs/MODEL.md +0 -296
- package/docs/PATTERNS.md +0 -182
- package/docs/SERVER.md +0 -88
- package/docs/UTILITIES.md +0 -111
package/docs/MODEL.md
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
# Model Layer
|
|
2
|
-
|
|
3
|
-
`Model extends Core`. Everything from the [Core layer](./CORE.md) is available on a `Model` instance — `insert`, `update`, `delete`, `get`, `list`, `find`, `getSchema`, `pull`. On top of that, `Model` adds the behavior surface: hooks, workers, methods, uploads, streams, events, subscriptions, logging, and extensions.
|
|
4
|
-
|
|
5
|
-
> [← Back to README](../README.md)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
- [Hooks](#hooks)
|
|
12
|
-
- [Workers](#workers)
|
|
13
|
-
- [Methods](#methods)
|
|
14
|
-
- [Uploads](#uploads)
|
|
15
|
-
- [Streams](#streams)
|
|
16
|
-
- [Events & Subscriptions](#events--subscriptions)
|
|
17
|
-
- [Internal Event Emitter](#internal-event-emitter)
|
|
18
|
-
- [Extensions](#extensions)
|
|
19
|
-
- [Logging](#logging)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Hooks
|
|
24
|
-
|
|
25
|
-
Hooks run before or after a CRUD operation. They can short-circuit by throwing, mutate the payload, or perform side effects. Both sync and async callbacks are supported.
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
users.hook({
|
|
29
|
-
id : 'normalize-email',
|
|
30
|
-
on : 'before:insert',
|
|
31
|
-
callback : async ({ data }) => {
|
|
32
|
-
if (data?.email) data.email = data.email.toLowerCase();
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
users.hook({
|
|
37
|
-
id : 'audit',
|
|
38
|
-
on : ['after:insert', 'after:update', 'after:delete'],
|
|
39
|
-
callback : ({ operation, key, data }) => audit.log(operation, key, data)
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
users.unhook('normalize-email');
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Hook events
|
|
46
|
-
|
|
47
|
-
| Event | Payload fields |
|
|
48
|
-
|-------------------|-----------------------------------------------|
|
|
49
|
-
| `before:insert` | `operation`, `timing`, `data` |
|
|
50
|
-
| `after:insert` | `operation`, `timing`, `data`, `key: { _id }` |
|
|
51
|
-
| `before:update` | `operation`, `timing`, `key`, `data` |
|
|
52
|
-
| `after:update` | `operation`, `timing`, `key`, `data` |
|
|
53
|
-
| `before:delete` | `operation`, `timing`, `key` |
|
|
54
|
-
| `after:delete` | `operation`, `timing`, `key`, `data` |
|
|
55
|
-
| `before:list` | `operation`, `timing`, `data` (= query) |
|
|
56
|
-
| `after:list` | `operation`, `timing`, `data` (= query) |
|
|
57
|
-
|
|
58
|
-
`get` and `find` do **not** fire hooks.
|
|
59
|
-
|
|
60
|
-
### Scheduled hooks
|
|
61
|
-
|
|
62
|
-
`schedule: <ms>` defers execution via `setTimeout`. Scheduled hooks are fire-and-forget — they don't block the operation and their errors aren't propagated.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
users.hook({
|
|
66
|
-
id : 'welcome-email',
|
|
67
|
-
on : 'after:insert',
|
|
68
|
-
schedule : 5_000,
|
|
69
|
-
callback : ({ data }) => mailer.send(data.email, 'welcome')
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Throwing from a hook
|
|
74
|
-
|
|
75
|
-
A non-scheduled hook that throws aborts the operation. The error is logged at `error` level and re-thrown to the caller.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Workers
|
|
80
|
-
|
|
81
|
-
Workers are interval-based background tasks bound to the model. They can be **named** (managed individually) or **unsigned** (managed in bulk).
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
users.worker({
|
|
85
|
-
id : 'sync',
|
|
86
|
-
interval : 30_000,
|
|
87
|
-
immediate : true, // run once on start, default true
|
|
88
|
-
handler : async (model) => { await model.pull(); }
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
users.worker({ interval: 60_000, handler: () => cleanup() }); // unsigned
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Lifecycle
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
users.start(); // start all (named + unsigned)
|
|
98
|
-
users.start('sync'); // start a specific named worker
|
|
99
|
-
users.stop(); // stop all
|
|
100
|
-
users.stop('sync'); // stop a specific named worker
|
|
101
|
-
users.isRunning('sync'); // → boolean
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Errors thrown inside a worker handler are caught and logged — they never crash the interval.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## Methods
|
|
109
|
-
|
|
110
|
-
Methods are named, schemaless RPC-style operations. Use them for anything that doesn't fit the CRUD shape.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
users.method({
|
|
114
|
-
id : 'resetPassword',
|
|
115
|
-
handler : async ({ email, newPassword }, model, ctx) => {
|
|
116
|
-
const u = await model.get({ email });
|
|
117
|
-
if (!u) throw { code: 'NOT_FOUND' };
|
|
118
|
-
return model.update({ _id: u._id }, { password: newPassword });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await users.call('resetPassword', { email, newPassword }, ctx);
|
|
123
|
-
users.getMethods(); // → ['resetPassword']
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
`call` throws `{ code: 'METHOD_NOT_FOUND', id }` if the method isn't registered.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Uploads
|
|
131
|
-
|
|
132
|
-
Uploads receive a `Buffer` plus a `fields` map (typically multipart form fields).
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
users.upload({
|
|
136
|
-
id : 'avatar',
|
|
137
|
-
handler : async (file, fields, model) => {
|
|
138
|
-
const path = await storage.put(file, fields.filename);
|
|
139
|
-
return model.update({ _id: fields.userId }, { avatar: path });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
await users.callUpload('avatar', buffer, { userId, filename: 'me.png' });
|
|
144
|
-
users.getUploads(); // → ['avatar']
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
## Streams
|
|
150
|
-
|
|
151
|
-
Streams are similar to uploads but expect chunked `Buffer` data with arbitrary `meta`.
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
users.stream({
|
|
155
|
-
id : 'import',
|
|
156
|
-
handler : async (chunk, meta, model) => {
|
|
157
|
-
for (const row of parseCsv(chunk)) await model.insert(row);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await users.callStream('import', chunk, { source: 'crm' });
|
|
162
|
-
users.getStreams(); // → ['import']
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## Events & Subscriptions
|
|
168
|
-
|
|
169
|
-
Events are named, schema-validated handlers that clients **subscribe to**. Where `method` is one-shot RPC, events are designed for fan-out to multiple subscribers, scoped by query.
|
|
170
|
-
|
|
171
|
-
### Defining an event
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
users.event({
|
|
175
|
-
id : 'roleChanged',
|
|
176
|
-
schema : {
|
|
177
|
-
type : 'object',
|
|
178
|
-
required : ['_id', 'role'],
|
|
179
|
-
properties: {
|
|
180
|
-
_id : { type: 'string' },
|
|
181
|
-
role : { type: 'string', enum: ['user', 'admin'] }
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
handler: async ({ _id, role }, model, ctx) => {
|
|
185
|
-
return model.update({ _id }, { role });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
If `schema` is set, args are validated before the handler runs. Validation errors throw `{ code: 'VALIDATION_ERROR', errors }`.
|
|
191
|
-
|
|
192
|
-
### Subscribing
|
|
193
|
-
|
|
194
|
-
A subscription is keyed by a normalized query — same query keys/values produce the same subscription instance.
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
const { subscription, subId } = users.subscribe({ role: 'admin' });
|
|
198
|
-
|
|
199
|
-
subscription.on('publish', payload => {
|
|
200
|
-
console.log('admin update:', payload);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// shorthand — returns just the subscription
|
|
204
|
-
const sub = users.subscription({ role: 'admin' });
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Dispatching an event
|
|
208
|
-
|
|
209
|
-
`dispatch` validates args (if a schema is set) and runs the matching event handler.
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
await sub.dispatch('roleChanged', { _id, role: 'admin' }, ctx);
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Throws `{ code: 'EVENT_NOT_FOUND', id }` if the event isn't registered.
|
|
216
|
-
|
|
217
|
-
### Publishing to subscribers
|
|
218
|
-
|
|
219
|
-
`publish` is your fan-out hook — it emits to every listener attached via `on('publish', ...)` for that subscription. The transport layer ([Server](./SERVER.md)) wires this into a Socket.IO room so browser clients receive it automatically.
|
|
220
|
-
|
|
221
|
-
```ts
|
|
222
|
-
sub.publish({ type: 'role-changed', _id, role: 'admin' });
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
A common pattern is calling `publish` from inside an `after:update` hook to notify everyone watching that slice of the model. See [Patterns](./PATTERNS.md).
|
|
226
|
-
|
|
227
|
-
### Inspection / cleanup
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
sub.getEvents(); // → ['roleChanged']
|
|
231
|
-
users.unsubscribe({ role: 'admin' });
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## Internal Event Emitter
|
|
237
|
-
|
|
238
|
-
Separate from the subscription system, every model is also an `EventEmitter`. CRUD operations emit on this emitter directly — useful for in-process listeners that don't need query scoping.
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
users.on('insert', doc => log('inserted', doc));
|
|
242
|
-
users.on('update', ({ key, data }) => log('updated', key, data));
|
|
243
|
-
users.on('delete', result => log('deleted', result));
|
|
244
|
-
users.on('log', entry => sink.write(entry));
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`on`, `off`, `once`, `emit` are all available. Built-in events: `insert`, `update`, `delete`, `log`.
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Extensions
|
|
252
|
-
|
|
253
|
-
Pass `extensions: [...]` in the constructor options to mount sub-models as named properties on the parent. The mount key is `extension.id`.
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
const ssh = new Model('ssh', sshSchema);
|
|
257
|
-
const users = new Model('users', userSchema, { extensions: [ssh] });
|
|
258
|
-
|
|
259
|
-
users.ssh.insert({ host: '...', port: 22, username: 'root' });
|
|
260
|
-
users.getExtensions(); // → [Model('ssh')]
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
This is how Modeller composes namespaces — a `system` model with `system.ssh`, `system.docker`, `system.firewall` extensions, all addressable as `system.ssh.insert(...)`.
|
|
264
|
-
|
|
265
|
-
---
|
|
266
|
-
|
|
267
|
-
## Logging
|
|
268
|
-
|
|
269
|
-
A built-in logger surfaces hook, worker, method, event, and CRUD activity.
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
const users = new Model('users', schema, {
|
|
273
|
-
level : 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
274
|
-
timestamp : true,
|
|
275
|
-
features : { hook: true, worker: true, method: true, event: false, crud: true },
|
|
276
|
-
onEntry : entry => myLogger.log(entry)
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Or supply a custom adapter:
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
{ logAdapter: { log: entry => myStore.append(entry) } }
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Listen on the model itself:
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
users.on('log', entry => sink.write(entry));
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Each entry includes `level`, `feature`, `modelId`, `message`, `callSite`, `timestamp`, and optional `data`. `initial` is `true` for one-shot registration messages (e.g. "Hook registered"), `false` for runtime activity.
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
> **Next:** [Server layer →](./SERVER.md)
|
package/docs/PATTERNS.md
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# Common Patterns
|
|
2
|
-
|
|
3
|
-
Recipes that show the [Core → Model → Server](../README.md) chain in action. Each pattern is small, self-contained, and reflects how the framework is meant to be composed.
|
|
4
|
-
|
|
5
|
-
> [← Back to README](../README.md)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Publish on update
|
|
10
|
-
|
|
11
|
-
Notify every client subscribed to a slice of a model whenever a document in that slice changes. The `after:update` hook is the natural seam.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
users.hook({
|
|
15
|
-
id: 'broadcast',
|
|
16
|
-
on: 'after:update',
|
|
17
|
-
callback: ({ key, data }) => {
|
|
18
|
-
users.subscription({ _id: key._id }).publish({ type: 'updated', data });
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
The [Server layer](./SERVER.md) wires the subscription into a Socket.IO room, so every browser client subscribed to `{ _id }` receives the payload automatically.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## FK validation across models
|
|
28
|
-
|
|
29
|
-
Use the async `model` keyword to require that a field references an existing document in another model.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
const orders = new Model('orders', {
|
|
33
|
-
type: 'object',
|
|
34
|
-
properties: {
|
|
35
|
-
userId: { type: 'string', model: 'users.email' }
|
|
36
|
-
}
|
|
37
|
-
}, { schemes: [users.getSchema()] });
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`Validator` rejects an `insert` whose `userId` doesn't exist in the `users` model. See [Validator](./CORE.md#validator).
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Composing a system root via extensions
|
|
45
|
-
|
|
46
|
-
`extensions` mounts sub-models as named properties on the parent — addressable as `parent.childId.method(...)`.
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
const ssh = new Model('ssh', sshSchema);
|
|
50
|
-
const docker = new Model('docker', dockerSchema);
|
|
51
|
-
const firewall = new Model('firewall', firewallSchema);
|
|
52
|
-
|
|
53
|
-
const system = new Model('system', systemSchema, {
|
|
54
|
-
extensions: [ssh, docker, firewall]
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
await system.ssh.insert({ host: '...', port: 22, username: 'root' });
|
|
58
|
-
await system.firewall.list();
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
This pattern is how Modeller composes namespaces. Each extension keeps its own hooks, workers, methods, events, and schema — they aren't merged, just mounted.
|
|
62
|
-
|
|
63
|
-
See [Extensions](./MODEL.md#extensions).
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Background sync worker
|
|
68
|
-
|
|
69
|
-
Reconcile in-memory state with an external source on an interval.
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
firewall.worker({
|
|
73
|
-
id : 'sync-rules',
|
|
74
|
-
interval : 30_000,
|
|
75
|
-
handler : async (model) => {
|
|
76
|
-
const live = await readLiveUFW();
|
|
77
|
-
for (const rule of live) {
|
|
78
|
-
const existing = await model.get({ id: rule.id });
|
|
79
|
-
if (!existing) await model.insert(rule);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
firewall.start('sync-rules');
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Errors inside the handler are logged but never crash the interval. See [Workers](./MODEL.md#workers).
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Quota / authorization in `before:insert`
|
|
92
|
-
|
|
93
|
-
Throw from a `before:` hook to abort an operation.
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
users.hook({
|
|
97
|
-
id: 'check-quota',
|
|
98
|
-
on: 'before:insert',
|
|
99
|
-
callback: async ({ data }) => {
|
|
100
|
-
if (await overQuota(data.tenantId)) {
|
|
101
|
-
throw { code: 'QUOTA_EXCEEDED' };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
The thrown object propagates back to the caller of `insert()`. See [Errors](./ERRORS.md).
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Schedule a side effect after insert
|
|
112
|
-
|
|
113
|
-
Send a welcome email five seconds after a user is created — without blocking the insert.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
users.hook({
|
|
117
|
-
id : 'welcome-email',
|
|
118
|
-
on : 'after:insert',
|
|
119
|
-
schedule : 5_000,
|
|
120
|
-
callback : ({ data }) => mailer.send(data.email, 'welcome')
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
`schedule` makes the hook fire-and-forget via `setTimeout`. Errors from scheduled hooks are logged but don't propagate.
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
## Persist with an adapter, hydrate on boot
|
|
129
|
-
|
|
130
|
-
Wire an `Adapter` so writes are durable, then `pull()` once at startup.
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
class FileAdapter extends Adapter {
|
|
134
|
-
async sync(id, doc) { await fs.writeFile(`./data/${id}.json`, JSON.stringify(doc)); }
|
|
135
|
-
async pull() { return JSON.parse(await fs.readFile('./data/users.json', 'utf8')); }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const users = new Model('users', schema, { adapter: FileAdapter });
|
|
139
|
-
await users.pull(); // hydrate before serving traffic
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
See [Adapter](./CORE.md#adapter).
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Method as RPC, event as fan-out
|
|
147
|
-
|
|
148
|
-
Two different shapes for "do something":
|
|
149
|
-
|
|
150
|
-
- **Method** — one caller, one return value. Use for actions like `resetPassword`, `regenerateApiKey`.
|
|
151
|
-
- **Event** — one or many callers via a subscription, validated args, multi-subscriber fan-out. Use when the action should also notify watchers.
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
// method — RPC
|
|
155
|
-
users.method({ id: 'resetPassword', handler: async (...) => { /* ... */ } });
|
|
156
|
-
|
|
157
|
-
// event — broadcasts via subscription
|
|
158
|
-
users.event({ id: 'roleChanged', schema, handler: async (...) => { /* ... */ } });
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
If in doubt, start with a method. Promote to an event only when more than one consumer needs to know it happened.
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## Listening in-process vs over the wire
|
|
166
|
-
|
|
167
|
-
Two emitters live on every model. Use the right one for the job:
|
|
168
|
-
|
|
169
|
-
- **`model.on('insert' | 'update' | 'delete' | 'log', ...)`** — in-process, no query scoping. Use for audit logs, metrics, internal indexers.
|
|
170
|
-
- **`model.subscription(query).on('publish', ...)`** — query-scoped fan-out, designed to be bridged to clients via the [Server layer](./SERVER.md).
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
// in-process: every write
|
|
174
|
-
users.on('update', ({ key, data }) => metrics.increment('users.updated'));
|
|
175
|
-
|
|
176
|
-
// per-query: clients listening for admin changes
|
|
177
|
-
users.subscription({ role: 'admin' }).on('publish', payload => doSomething(payload));
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
> **Related:** [Model →](./MODEL.md) · [Errors →](./ERRORS.md)
|
package/docs/SERVER.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Server Layer
|
|
2
|
-
|
|
3
|
-
`Server` is a thin transport that exposes models over HTTP and WebSocket. The package abstracts the underlying Koa + Socket.IO + OIDC + busboy machinery — you don't interact with it directly.
|
|
4
|
-
|
|
5
|
-
> [← Back to README](../README.md)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## The mental model
|
|
10
|
-
|
|
11
|
-
- **Models in, HTTP + WS out.** Methods, uploads, streams, and events become endpoints.
|
|
12
|
-
- **Subscriptions become rooms.** `subscription.publish(payload)` reaches every connected client subscribed to that query.
|
|
13
|
-
- **A `/@metadata` endpoint** advertises every model, scope, operation, and event so clients can build typed proxies at runtime.
|
|
14
|
-
- **Auth is OIDC + JWT.** Routes can opt in with `protected: true`.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Quick example
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { Server, Model } from '@hotfusion/modeller';
|
|
22
|
-
|
|
23
|
-
const server = new Server(3030, { domain: 'api', secret: process.env.SECRET });
|
|
24
|
-
|
|
25
|
-
server.route({
|
|
26
|
-
method : 'post',
|
|
27
|
-
path : '/system/ping',
|
|
28
|
-
callback : async (ctx) => ({ ok: true, ts: Date.now() })
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
server.start();
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Public methods
|
|
37
|
-
|
|
38
|
-
| Method | Purpose |
|
|
39
|
-
|-------------------------------------------------|-------------------------------------------------------|
|
|
40
|
-
| `route({ method, path, callback, protected? })` | Register a custom HTTP route. |
|
|
41
|
-
| `upload(path, handler)` | Multipart form-data endpoint, parsed into a Buffer. |
|
|
42
|
-
| `stream(handler)` | Single global handler for chunked socket uploads with resume. |
|
|
43
|
-
| `setOIDCProviders(path, providers)` | Wire OIDC providers under a base path. |
|
|
44
|
-
| `start()` | Bind to the port, mount routes, start Socket.IO. |
|
|
45
|
-
|
|
46
|
-
The full HTTP/WS surface is generated from your models — you don't need to wire each operation manually.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Server events
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
server.on('mounted', (s) => console.log('listening'));
|
|
54
|
-
server.on('connection', ({ socket }) => /* socket.io connection */);
|
|
55
|
-
server.on('request', ({ ctx, flags }) => /* every HTTP request */);
|
|
56
|
-
server.on('authenticated', ({ profile, provider, ctx, middleware }) => /* OIDC success */);
|
|
57
|
-
server.on('provider', ({ token, sid, session }) => /* token issued */);
|
|
58
|
-
server.on('uploaded', ({ file, buffer, resolve }) => resolve({ /* meta */ }));
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
| Event | When it fires |
|
|
62
|
-
|-----------------|------------------------------------------------------------|
|
|
63
|
-
| `mounted` | After `start()` has bound the port and mounted routes. |
|
|
64
|
-
| `connection` | A new Socket.IO client connects. |
|
|
65
|
-
| `request` | Every incoming HTTP request, with `protected` flag. |
|
|
66
|
-
| `authenticated` | OIDC profile successfully verified. |
|
|
67
|
-
| `provider` | A provider has issued a token; session is established. |
|
|
68
|
-
| `uploaded` | A streamed upload finished assembling. Resolve with meta. |
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Auth
|
|
73
|
-
|
|
74
|
-
`{ protected: true }` on a route requires a valid `Authorization: Bearer <jwt>` header. The token is verified against the secret passed to the constructor (or auto-generated via `KeyGen.getSystemSecret()` if omitted).
|
|
75
|
-
|
|
76
|
-
`setOIDCProviders` wires multiple OIDC providers under a base path. The server handles the redirect, callback, token exchange, and session creation. Listen to `authenticated` and `provider` to react to login.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Scope
|
|
81
|
-
|
|
82
|
-
This package is meant to be used as an abstraction. The Server layer intentionally keeps a narrow surface so consumers don't need to know Koa, Socket.IO, OIDC client config, or busboy internals.
|
|
83
|
-
|
|
84
|
-
If you need to dig deeper — custom middleware, custom socket rooms, OIDC introspection — read `src/server.ts` directly. The public API documented here is the contract.
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
> **Related:** [Utilities →](./UTILITIES.md) · [Errors →](./ERRORS.md)
|
package/docs/UTILITIES.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Utilities
|
|
2
|
-
|
|
3
|
-
Helpers exported from `@hotfusion/modeller` and from `src/utils`.
|
|
4
|
-
|
|
5
|
-
> [← Back to README](../README.md)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Public exports
|
|
10
|
-
|
|
11
|
-
| Export | Purpose |
|
|
12
|
-
|-----------|------------------------------------------------------------------------|
|
|
13
|
-
| `Bundler` | Rollup-based bundler for shipping client code (TS, JSON, LESS, terser).|
|
|
14
|
-
| `KeyGen` | System-wide secret generator. |
|
|
15
|
-
| `Adapter` | Base class for persistence — extend to plug in a backing store. |
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## `Bundler`
|
|
20
|
-
|
|
21
|
-
A Rollup wrapper preconfigured for the Modeller stack. Bundles TypeScript, inlines JSON, compiles LESS via `rollup-plugin-postcss`, supports `@rollup/plugin-replace` for build-time substitutions, and minifies with terser.
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { Bundler } from '@hotfusion/modeller';
|
|
25
|
-
|
|
26
|
-
const result = await Bundler.bundle({
|
|
27
|
-
entry : 'src/client/index.ts',
|
|
28
|
-
output : 'dist/client.js'
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Use it for shipping client bundles served by the [Server layer](./SERVER.md), or for any pre-built artifact you want to expose at runtime.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## `KeyGen`
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import { KeyGen } from '@hotfusion/modeller';
|
|
40
|
-
|
|
41
|
-
const secret = KeyGen.getSystemSecret(); // → hex string
|
|
42
|
-
const bytes = KeyGen.getSystemSecret('uint8Array'); // → Uint8Array
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`getSystemSecret()` returns a stable 32-byte secret. On first call it generates one and caches it to disk (`_secret.key`); subsequent calls return the same value. Use it as the JWT signing key, the session secret, or wherever a stable per-installation secret is needed.
|
|
46
|
-
|
|
47
|
-
`KeyGen.generateSecret(false)` returns a fresh random secret each time — use this for one-off keys (per-document encryption, single-use tokens).
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## `Adapter`
|
|
52
|
-
|
|
53
|
-
Base class for persistence. See the full contract in the [Core layer doc](./CORE.md#adapter).
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { Adapter } from '@hotfusion/modeller';
|
|
57
|
-
|
|
58
|
-
class MyAdapter extends Adapter {
|
|
59
|
-
async sync(id, doc) { /* persist */ }
|
|
60
|
-
async pull() { /* return docs */ return []; }
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Internal utilities
|
|
67
|
-
|
|
68
|
-
These live in `src/utils` and aren't re-exported from the package root, but are stable and safe to import directly.
|
|
69
|
-
|
|
70
|
-
### `encrypt` / `decrypt`
|
|
71
|
-
|
|
72
|
-
AES-256-GCM wrappers using a `Uint8Array` secret.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { encrypt, decrypt } from '@hotfusion/modeller/dist/utils/encryption';
|
|
76
|
-
|
|
77
|
-
const hash = await encrypt('plaintext', secretBytes);
|
|
78
|
-
const plain = await decrypt(hash, secretBytes);
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Pair with `KeyGen.getSystemSecret('uint8Array')` for the secret.
|
|
82
|
-
|
|
83
|
-
### `signJWT` / `decodeJWT`
|
|
84
|
-
|
|
85
|
-
`jose`-backed JWT helpers.
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { signJWT } from '@hotfusion/modeller/dist/utils/sign-jwt';
|
|
89
|
-
import { decodeJWT } from '@hotfusion/modeller/dist/utils/decode-jwt';
|
|
90
|
-
|
|
91
|
-
const token = await signJWT(payload, secret, { exp: '1h' });
|
|
92
|
-
const claims = decodeJWT(token);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### `request`
|
|
96
|
-
|
|
97
|
-
Minimal `fetch` wrapper used internally by the connector. Returns the parsed body, throws on non-2xx.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { request } from '@hotfusion/modeller/dist/utils/request';
|
|
101
|
-
|
|
102
|
-
const data = await request('https://api.example.com/x', { method: 'GET' });
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### `ObjectCollection`
|
|
106
|
-
|
|
107
|
-
A typed `Map`-with-extras used internally for keyed collections. Useful when you need a `Map` plus filtering / iteration helpers.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
> **Related:** [Core →](./CORE.md) · [Errors →](./ERRORS.md)
|