@cero-base/core 0.2.0 → 0.4.0
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/LICENSE +201 -0
- package/README.md +180 -136
- package/package.json +85 -26
- package/src/blobs/index.js +297 -0
- package/src/database/CLAUDE.md +3 -0
- package/src/database/bootstrap.js +76 -0
- package/src/database/dispatch.js +156 -0
- package/src/database/index.js +572 -0
- package/src/identity/CLAUDE.md +3 -0
- package/src/identity/index.js +232 -0
- package/src/index.js +20 -1
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/constants.js +24 -4
- package/src/lib/errors.js +150 -0
- package/src/lib/schema.js +58 -440
- package/src/lib/spec/index.js +353 -0
- package/src/lib/spec/schema.json +284 -0
- package/src/lib/utils.js +54 -49
- package/src/network/discovery.js +80 -0
- package/src/network/index.js +231 -0
- package/src/pairing/index.js +482 -0
- package/src/pairing/invite.js +199 -0
- package/src/rpc/client.js +45 -0
- package/src/rpc/index.js +141 -0
- package/src/rpc/server.js +45 -0
- package/src/storage/index.js +261 -0
- package/types/blobs/index.d.ts +169 -0
- package/types/database/bootstrap.d.ts +17 -0
- package/types/database/dispatch.d.ts +8 -0
- package/types/database/index.d.ts +329 -0
- package/types/identity/index.d.ts +160 -0
- package/types/index.d.ts +11 -0
- package/types/lib/constants.d.ts +13 -0
- package/types/lib/errors.d.ts +110 -0
- package/types/lib/schema.d.ts +53 -0
- package/types/lib/spec/index.d.ts +95 -0
- package/types/lib/utils.d.ts +39 -0
- package/types/network/discovery.d.ts +44 -0
- package/types/network/index.d.ts +115 -0
- package/types/pairing/index.d.ts +194 -0
- package/types/pairing/invite.d.ts +157 -0
- package/types/rpc/client.d.ts +18 -0
- package/types/rpc/index.d.ts +67 -0
- package/types/rpc/server.d.ts +18 -0
- package/types/storage/index.d.ts +163 -0
- package/src/lib/base.js +0 -84
- package/src/lib/batch.js +0 -98
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -252
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -145
package/src/lib/schema.js
CHANGED
|
@@ -1,450 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { extendRoomSchema, extendRoomDispatch } from '@lekinox/cero/src/room/schema.js'
|
|
4
|
-
import { singular, toFields, scopeNs, isLocal, isPrivate, isShared } from './utils.js'
|
|
5
|
-
import { SCOPE_LOCAL, SCOPE_PRIVATE, SCOPE_SHARED } from './constants.js'
|
|
1
|
+
import { CeroError } from './errors.js'
|
|
2
|
+
import { SINGLE, COLLECTION, ACTION } from './constants.js'
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// table at schema-build time (see ceroDispatches), not hardcoded here.
|
|
14
|
-
const BUILTINS = {
|
|
15
|
-
members: { scope: SCOPE_SHARED, type: 'member', key: ['id'] },
|
|
16
|
-
profile: { scope: SCOPE_PRIVATE, type: 'profile', key: [] },
|
|
17
|
-
devices: { scope: SCOPE_PRIVATE, type: 'device', key: ['id'] },
|
|
18
|
-
rooms: { scope: SCOPE_PRIVATE, type: 'room', key: ['id'] },
|
|
19
|
-
invites: { scope: SCOPE_PRIVATE, type: 'invite', key: ['id'] },
|
|
20
|
-
mirrors: { scope: SCOPE_PRIVATE, type: 'mirror', key: ['key'] },
|
|
21
|
-
settings: { scope: SCOPE_PRIVATE, type: 'setting', key: ['key'] }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Local-scope built-ins cero registers in @local/* — exposed to apps via
|
|
25
|
-
// db.local.collection(name). Distinct from BUILTINS (which are private/shared).
|
|
26
|
-
const LOCAL_BUILTINS = {
|
|
27
|
-
identity: { type: 'identity', key: [] },
|
|
28
|
-
rooms: { type: 'room', key: ['key'] },
|
|
29
|
-
files: { type: 'file', key: ['key', 'blob.blockOffset'] },
|
|
30
|
-
settings: { type: 'setting', key: ['key'] },
|
|
31
|
-
counters: { type: 'counter', key: ['name'] }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// One in-memory Hyperschema per (scope, ns) — built once by running cero's
|
|
35
|
-
// extender and reused for every type lookup. The extender registers all
|
|
36
|
-
// built-in types in the namespace; baseFieldsOf just reads them back.
|
|
37
|
-
const schemaCache = new Map()
|
|
38
|
-
function builtinSchema(scope, nsName) {
|
|
39
|
-
const key = `${scope}:${nsName}`
|
|
40
|
-
let schema = schemaCache.get(key)
|
|
41
|
-
if (schema) return schema
|
|
42
|
-
|
|
43
|
-
schema = new Hyperschema()
|
|
44
|
-
const ns = schema.namespace(nsName)
|
|
45
|
-
if (scope === SCOPE_SHARED) {
|
|
46
|
-
extendRoomSchema(schema, ns)
|
|
47
|
-
} else {
|
|
48
|
-
extendIdentitySchema(schema, ns)
|
|
49
|
-
}
|
|
50
|
-
schemaCache.set(key, schema)
|
|
51
|
-
return schema
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function baseFieldsOf(scope, nsName, typeName) {
|
|
55
|
-
const type = builtinSchema(scope, nsName).types.get(`@${nsName}/${typeName}`)
|
|
56
|
-
if (!type) throw new Error(`Built-in type '${typeName}' not found in cero schema`)
|
|
57
|
-
return type.toJSON().fields
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Cache of dispatch names cero registers per scope. Runs cero's extender
|
|
61
|
-
// against a stub namespace that just captures register() calls.
|
|
62
|
-
const dispatchCache = new Map()
|
|
63
|
-
function ceroDispatches(scope) {
|
|
64
|
-
if (dispatchCache.has(scope)) return dispatchCache.get(scope)
|
|
65
|
-
const captured = new Set()
|
|
66
|
-
const stubNs = {
|
|
67
|
-
name: scope === SCOPE_SHARED ? 'room' : 'identity',
|
|
68
|
-
register: (desc) => captured.add(desc.name)
|
|
69
|
-
}
|
|
70
|
-
if (scope === SCOPE_SHARED) {
|
|
71
|
-
extendRoomDispatch(null, stubNs)
|
|
72
|
-
} else {
|
|
73
|
-
extendIdentityDispatch(null, stubNs)
|
|
74
|
-
}
|
|
75
|
-
dispatchCache.set(scope, captured)
|
|
76
|
-
return captured
|
|
77
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ prim: string }} Prim
|
|
6
|
+
* @typedef {{ kind: 'single' | 'collection' | 'action', fields: Record<string, Prim> }} TypeDef
|
|
7
|
+
* @typedef {{ [name: string]: TypeDef | Record<string, TypeDef> } & { local?: Record<string, TypeDef> }} SchemaDefs
|
|
8
|
+
* @typedef {{ defs: SchemaDefs }} Schema
|
|
9
|
+
*/
|
|
78
10
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return ceroDispatches(scope).has(`add-${typeName}`)
|
|
82
|
-
}
|
|
11
|
+
/** @param {string} name @returns {Prim} */
|
|
12
|
+
const prim = (name) => ({ prim: name })
|
|
83
13
|
|
|
84
14
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
15
|
+
* Schema DSL. Primitive field types and kind-constructors used to describe
|
|
16
|
+
* a database's tables before the builder turns them into a wire spec.
|
|
87
17
|
*/
|
|
88
18
|
export const t = {
|
|
89
|
-
string:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fixed32:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
19
|
+
string: prim('string'),
|
|
20
|
+
uint: prim('uint'),
|
|
21
|
+
int: prim('int'),
|
|
22
|
+
bool: prim('bool'),
|
|
23
|
+
bytes: prim('bytes'),
|
|
24
|
+
json: prim('json'),
|
|
25
|
+
fixed32: prim('fixed32'),
|
|
26
|
+
fixed64: prim('fixed64'),
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Single-row table (one record, set/get by name).
|
|
30
|
+
*
|
|
31
|
+
* @param {Record<string, Prim>} fields
|
|
32
|
+
* @returns {TypeDef}
|
|
33
|
+
*/
|
|
34
|
+
single(fields) {
|
|
35
|
+
return { kind: SINGLE, fields }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Multi-row table keyed by `id`.
|
|
40
|
+
*
|
|
41
|
+
* @param {Record<string, Prim>} fields
|
|
42
|
+
* @returns {TypeDef}
|
|
43
|
+
*/
|
|
44
|
+
collection(fields) {
|
|
45
|
+
return { kind: COLLECTION, fields }
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* RPC-style mutation that doesn't persist a row.
|
|
50
|
+
*
|
|
51
|
+
* @param {Record<string, Prim>} fields
|
|
52
|
+
* @returns {TypeDef}
|
|
53
|
+
*/
|
|
54
|
+
action(fields) {
|
|
55
|
+
return { kind: ACTION, fields }
|
|
110
56
|
}
|
|
111
|
-
return fields
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function inputFields(def) {
|
|
115
|
-
return toFields(def.fields).map((f) => ({ ...f, required: false }))
|
|
116
57
|
}
|
|
117
58
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const entries = Object.entries(collections)
|
|
128
|
-
|
|
129
|
-
// Validate built-in entries: user can't override fixed metadata (key, scope, timestamps).
|
|
130
|
-
for (const [name, def] of entries) {
|
|
131
|
-
if (!BUILTINS[name]) continue
|
|
132
|
-
if (def.key !== undefined) {
|
|
133
|
-
throw new Error(`'${name}' is a built-in; key is fixed and cannot be set`)
|
|
134
|
-
}
|
|
135
|
-
if (def.scope !== undefined && def.scope !== BUILTINS[name].scope) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`'${name}' is a built-in with scope '${BUILTINS[name].scope}'; cannot override`
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
if (def.timestamps !== undefined) {
|
|
141
|
-
throw new Error(`'${name}' is a built-in; timestamps is fixed and cannot be set`)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Type name resolution: built-ins use cero's singular type name, user collections use singular(plural).
|
|
146
|
-
const types = new Map([
|
|
147
|
-
// Pre-populate with built-ins so they resolve even when user didn't declare them.
|
|
148
|
-
...Object.entries(BUILTINS).map(([name, info]) => [name, info.type]),
|
|
149
|
-
...entries.map(([name]) => [name, BUILTINS[name]?.type || singular(name)])
|
|
150
|
-
])
|
|
151
|
-
|
|
152
|
-
const getScope = ([name, def]) =>
|
|
153
|
-
BUILTINS[name] ? BUILTINS[name].scope : def.scope || SCOPE_PRIVATE
|
|
154
|
-
const localEntries = entries.filter((e) => getScope(e) === SCOPE_LOCAL)
|
|
155
|
-
const privateEntries = entries.filter((e) => getScope(e) === SCOPE_PRIVATE)
|
|
156
|
-
const sharedEntries = entries.filter((e) => getScope(e) === SCOPE_SHARED)
|
|
157
|
-
|
|
158
|
-
const nsFor = (scope) => (scope === SCOPE_SHARED && opts.namespace) || scopeNs(scope)
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
namespace: opts.namespace || null,
|
|
162
|
-
collections,
|
|
163
|
-
types,
|
|
164
|
-
|
|
165
|
-
getScope(collection) {
|
|
166
|
-
if (BUILTINS[collection]) return BUILTINS[collection].scope
|
|
167
|
-
const def = collections[collection]
|
|
168
|
-
return def ? def.scope || SCOPE_PRIVATE : null
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
isBuiltin(collection) {
|
|
172
|
-
return !!BUILTINS[collection]
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Does Collection.put follow the add/set merge path for this collection?
|
|
177
|
-
*
|
|
178
|
-
* - User-defined: always yes. Non-local user collections get auto-generated
|
|
179
|
-
* `add-${type}` dispatches; local collections take the merge path even
|
|
180
|
-
* without dispatches (they write straight to the local store).
|
|
181
|
-
* - Built-ins: derived from cero's actual dispatch table — `false` for
|
|
182
|
-
* collections cero exposes only via `set-${type}` (e.g. profile, settings).
|
|
183
|
-
*/
|
|
184
|
-
hasAdd(collection) {
|
|
185
|
-
const builtin = BUILTINS[collection]
|
|
186
|
-
if (builtin) return ceroHasAdd(builtin.scope, builtin.type)
|
|
187
|
-
return !!collections[collection]
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
keysOf(collection) {
|
|
191
|
-
if (BUILTINS[collection]) return BUILTINS[collection].key
|
|
192
|
-
return collections[collection]?.key || ['id']
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
isLocalBuiltin(collection) {
|
|
196
|
-
return !!LOCAL_BUILTINS[collection]
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Resolve a name to its local-scope type. Returns null if the name doesn't
|
|
201
|
-
* map to a local collection (cero built-in or user-declared `scope: 'local'`).
|
|
202
|
-
*/
|
|
203
|
-
localTypeOf(collection) {
|
|
204
|
-
if (LOCAL_BUILTINS[collection]) return LOCAL_BUILTINS[collection].type
|
|
205
|
-
const def = collections[collection]
|
|
206
|
-
if (def?.scope === SCOPE_LOCAL) return singular(collection)
|
|
207
|
-
return null
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
localKeysOf(collection) {
|
|
211
|
-
if (LOCAL_BUILTINS[collection]) return LOCAL_BUILTINS[collection].key
|
|
212
|
-
const def = collections[collection]
|
|
213
|
-
if (def?.scope === SCOPE_LOCAL) return def.key || ['id']
|
|
214
|
-
return null
|
|
215
|
-
},
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Resolve a local collection name to its hyperdb spec + path. Returns null
|
|
219
|
-
* if `name` isn't a local collection. Local built-ins don't have input-X
|
|
220
|
-
* types, so the prefix is ignored for them.
|
|
221
|
-
*/
|
|
222
|
-
localResolve(collection, prefix) {
|
|
223
|
-
const ns = scopeNs(SCOPE_LOCAL)
|
|
224
|
-
if (LOCAL_BUILTINS[collection]) {
|
|
225
|
-
return { spec: ns, path: `@${ns}/${LOCAL_BUILTINS[collection].type}` }
|
|
226
|
-
}
|
|
227
|
-
const def = collections[collection]
|
|
228
|
-
if (def?.scope === SCOPE_LOCAL) {
|
|
229
|
-
const type = singular(collection)
|
|
230
|
-
const name = prefix ? `${prefix}-${type}` : type
|
|
231
|
-
return { spec: ns, path: `@${ns}/${name}` }
|
|
232
|
-
}
|
|
233
|
-
return null
|
|
234
|
-
},
|
|
235
|
-
|
|
236
|
-
isLocal(collection) {
|
|
237
|
-
return isLocal(this.getScope(collection))
|
|
238
|
-
},
|
|
239
|
-
isPrivate(collection) {
|
|
240
|
-
return isPrivate(this.getScope(collection))
|
|
241
|
-
},
|
|
242
|
-
isShared(collection) {
|
|
243
|
-
return isShared(this.getScope(collection))
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
hasTimestamps(collection) {
|
|
247
|
-
return collections[collection]?.timestamps === true
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
hasCounter(collection) {
|
|
251
|
-
return collections[collection]?.counter === true
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
counterNames(scope) {
|
|
255
|
-
const items =
|
|
256
|
-
scope === SCOPE_SHARED
|
|
257
|
-
? sharedEntries
|
|
258
|
-
: scope === SCOPE_LOCAL
|
|
259
|
-
? localEntries
|
|
260
|
-
: privateEntries
|
|
261
|
-
// map: type name (singular) → collection name (plural)
|
|
262
|
-
const map = {}
|
|
263
|
-
for (const [name, def] of items) {
|
|
264
|
-
if (def.counter) map[types.get(name)] = name
|
|
265
|
-
}
|
|
266
|
-
return map
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
resolve(collection, prefix) {
|
|
270
|
-
const scope = this.getScope(collection)
|
|
271
|
-
const type = types.get(collection)
|
|
272
|
-
const name = prefix ? `${prefix}-${type}` : type
|
|
273
|
-
return { spec: scopeNs(scope), path: `@${nsFor(scope)}/${name}` }
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
require(collection, context) {
|
|
277
|
-
const scope = this.getScope(collection)
|
|
278
|
-
if (!scope) throw new Error(`Unknown collection: '${collection}'`)
|
|
279
|
-
if (context === 'db' && scope === SCOPE_SHARED) {
|
|
280
|
-
throw new Error(`'${collection}' is shared — use room.collection('${collection}')`)
|
|
281
|
-
}
|
|
282
|
-
if (context === 'room' && scope !== SCOPE_SHARED) {
|
|
283
|
-
throw new Error(`'${collection}' is ${scope} — use db.collection('${collection}')`)
|
|
284
|
-
}
|
|
285
|
-
return scope
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
getPath(collection) {
|
|
289
|
-
const scope = this.getScope(collection)
|
|
290
|
-
return `@${nsFor(scope)}/${collection}`
|
|
291
|
-
},
|
|
292
|
-
|
|
293
|
-
build(scope, register, nsName) {
|
|
294
|
-
const items =
|
|
295
|
-
scope === SCOPE_SHARED
|
|
296
|
-
? sharedEntries
|
|
297
|
-
: scope === SCOPE_LOCAL
|
|
298
|
-
? localEntries
|
|
299
|
-
: privateEntries
|
|
300
|
-
const local = scope === SCOPE_LOCAL
|
|
301
|
-
const ns = nsName || scopeNs(scope)
|
|
302
|
-
for (const [name, def] of items) {
|
|
303
|
-
if (BUILTINS[name]) {
|
|
304
|
-
// Built-in extension: re-register the type with extra fields appended.
|
|
305
|
-
// cero's later registration is idempotent and skips when the type already exists.
|
|
306
|
-
const typeName = BUILTINS[name].type
|
|
307
|
-
const baseFields = baseFieldsOf(scope, ns, typeName)
|
|
308
|
-
const userFields = toFields(def.fields || {}).map((f) => ({ ...f, required: false }))
|
|
309
|
-
|
|
310
|
-
const baseNames = new Set(baseFields.map((f) => f.name))
|
|
311
|
-
for (const uf of userFields) {
|
|
312
|
-
if (baseNames.has(uf.name)) {
|
|
313
|
-
throw new Error(
|
|
314
|
-
`'${uf.name}' is a base field of '${typeName}' and cannot be redeclared`
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const allFields = [...baseFields, ...userFields]
|
|
320
|
-
register.type({ name: typeName, compact: false, fields: allFields })
|
|
321
|
-
register.type({
|
|
322
|
-
name: `input-${typeName}`,
|
|
323
|
-
compact: false,
|
|
324
|
-
fields: allFields.map((f) => ({ ...f, required: false }))
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
if (def.indexes) {
|
|
328
|
-
for (const [idx, idxFields] of Object.entries(def.indexes)) {
|
|
329
|
-
register.index({
|
|
330
|
-
name: `${name}-${idx}`,
|
|
331
|
-
collection: `@${ns}/${name}`,
|
|
332
|
-
key: idxFields
|
|
333
|
-
})
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Register custom dispatches for user-supplied handlers.
|
|
338
|
-
// Use input-${type} (all-optional fields) so callers can pass partial
|
|
339
|
-
// payloads like { id } without re-supplying every base field.
|
|
340
|
-
if (def.handlers) {
|
|
341
|
-
for (const op of Object.keys(def.handlers)) {
|
|
342
|
-
register.dispatch({ name: op, requestType: `@${ns}/input-${typeName}` })
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
continue
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const type = types.get(name)
|
|
349
|
-
const fields = schemaFields(def)
|
|
350
|
-
const typePath = `@${ns}/${type}`
|
|
351
|
-
|
|
352
|
-
register.type({ name: type, compact: false, fields })
|
|
353
|
-
register.type({ name: `input-${type}`, compact: false, fields: inputFields(def) })
|
|
354
|
-
if (!local) register.type({ name: `del-${type}`, compact: false, fields: delFields(def) })
|
|
355
|
-
|
|
356
|
-
register.collection({
|
|
357
|
-
name,
|
|
358
|
-
schema: typePath,
|
|
359
|
-
key: def.key || ['id'],
|
|
360
|
-
counter: !!def.counter
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
if (def.indexes) {
|
|
364
|
-
for (const [idx, idxFields] of Object.entries(def.indexes)) {
|
|
365
|
-
register.index({ name: `${name}-${idx}`, collection: `@${ns}/${name}`, key: idxFields })
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!local) {
|
|
370
|
-
register.dispatch({ name: `add-${type}`, requestType: typePath })
|
|
371
|
-
register.dispatch({ name: `set-${type}`, requestType: typePath })
|
|
372
|
-
register.dispatch({ name: `del-${type}`, requestType: `@${ns}/del-${type}` })
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Register custom dispatches for user-supplied handlers.
|
|
376
|
-
// Use input-${type} so callers can pass partial payloads.
|
|
377
|
-
if (!local && def.handlers) {
|
|
378
|
-
for (const op of Object.keys(def.handlers)) {
|
|
379
|
-
register.dispatch({ name: op, requestType: `@${ns}/input-${type}` })
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Register input-${type} for implicit built-ins (not in user schema)
|
|
385
|
-
// so the RPC bridge can encode partial payloads for Collection writes.
|
|
386
|
-
if (!local) {
|
|
387
|
-
for (const [name, info] of Object.entries(BUILTINS)) {
|
|
388
|
-
if (info.scope !== scope) continue
|
|
389
|
-
if (collections[name]) continue // already handled by built-in branch above
|
|
390
|
-
const baseFields = baseFieldsOf(info.scope, ns, info.type)
|
|
391
|
-
register.type({
|
|
392
|
-
name: `input-${info.type}`,
|
|
393
|
-
compact: false,
|
|
394
|
-
fields: baseFields.map((f) => ({ ...f, required: false }))
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
routes(scope) {
|
|
401
|
-
const items = scope === SCOPE_PRIVATE ? privateEntries : sharedEntries
|
|
402
|
-
const handlers = {}
|
|
403
|
-
const p = (name) => `@${nsFor(scope)}/${name}`
|
|
404
|
-
for (const [name, def] of items) {
|
|
405
|
-
if (BUILTINS[name]) {
|
|
406
|
-
// Built-in extension: cero already owns add/set/del — only register custom handlers.
|
|
407
|
-
if (def.handlers) {
|
|
408
|
-
for (const [op, handler] of Object.entries(def.handlers)) {
|
|
409
|
-
handlers[p(op)] = handler
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
continue
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const type = types.get(name)
|
|
416
|
-
const path = p(name)
|
|
417
|
-
const keys = def.key || ['id']
|
|
418
|
-
|
|
419
|
-
handlers[p(`add-${type}`)] = async (op, ctx) => {
|
|
420
|
-
const counterPath = p('counters')
|
|
421
|
-
const counter = (await ctx.view.get(counterPath, { name })) || { name, value: 0 }
|
|
422
|
-
op.index = counter.value
|
|
423
|
-
counter.value++
|
|
424
|
-
await ctx.view.insert(counterPath, counter)
|
|
425
|
-
await ctx.view.insert(path, op)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
handlers[p(`set-${type}`)] = (op, ctx) => ctx.view.insert(path, op)
|
|
429
|
-
handlers[p(`del-${type}`)] = async (op, ctx) => {
|
|
430
|
-
await ctx.view.delete(
|
|
431
|
-
path,
|
|
432
|
-
keys.reduce((acc, k) => ((acc[k] = op[k]), acc), {})
|
|
433
|
-
)
|
|
434
|
-
const counterPath = p('counters')
|
|
435
|
-
const counter = (await ctx.view.get(counterPath, { name })) || { name, value: 0 }
|
|
436
|
-
counter.value = Math.max(0, counter.value - 1)
|
|
437
|
-
await ctx.view.insert(counterPath, counter)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (def.handlers) {
|
|
441
|
-
for (const [op, handler] of Object.entries(def.handlers)) {
|
|
442
|
-
handlers[p(op)] = handler
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return handlers
|
|
448
|
-
}
|
|
449
|
-
}
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a definitions object so the builder can recognise it as a schema.
|
|
61
|
+
*
|
|
62
|
+
* @param {SchemaDefs} defs
|
|
63
|
+
* @returns {Schema}
|
|
64
|
+
*/
|
|
65
|
+
export function schema(defs) {
|
|
66
|
+
if (!defs || typeof defs !== 'object') throw CeroError.INVALID('schema(defs) must be an object')
|
|
67
|
+
return { defs }
|
|
450
68
|
}
|