@cero-base/core 0.0.5 → 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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +200 -61
  3. package/package.json +85 -26
  4. package/src/blobs/index.js +297 -0
  5. package/src/database/CLAUDE.md +3 -0
  6. package/src/database/bootstrap.js +76 -0
  7. package/src/database/dispatch.js +156 -0
  8. package/src/database/index.js +572 -0
  9. package/src/identity/CLAUDE.md +3 -0
  10. package/src/identity/index.js +232 -0
  11. package/src/index.js +20 -1
  12. package/src/lib/CLAUDE.md +3 -0
  13. package/src/lib/constants.js +24 -4
  14. package/src/lib/errors.js +150 -0
  15. package/src/lib/schema.js +59 -194
  16. package/src/lib/spec/index.js +353 -0
  17. package/src/lib/spec/schema.json +284 -0
  18. package/src/lib/utils.js +54 -49
  19. package/src/network/discovery.js +80 -0
  20. package/src/network/index.js +231 -0
  21. package/src/pairing/index.js +482 -0
  22. package/src/pairing/invite.js +199 -0
  23. package/src/rpc/client.js +45 -0
  24. package/src/rpc/index.js +141 -0
  25. package/src/rpc/server.js +45 -0
  26. package/src/storage/index.js +261 -0
  27. package/types/blobs/index.d.ts +169 -0
  28. package/types/database/bootstrap.d.ts +17 -0
  29. package/types/database/dispatch.d.ts +8 -0
  30. package/types/database/index.d.ts +329 -0
  31. package/types/identity/index.d.ts +160 -0
  32. package/types/index.d.ts +11 -0
  33. package/types/lib/constants.d.ts +13 -0
  34. package/types/lib/errors.d.ts +110 -0
  35. package/types/lib/schema.d.ts +53 -0
  36. package/types/lib/spec/index.d.ts +95 -0
  37. package/types/lib/utils.d.ts +39 -0
  38. package/types/network/discovery.d.ts +44 -0
  39. package/types/network/index.d.ts +115 -0
  40. package/types/pairing/index.d.ts +194 -0
  41. package/types/pairing/invite.d.ts +157 -0
  42. package/types/rpc/client.d.ts +18 -0
  43. package/types/rpc/index.d.ts +67 -0
  44. package/types/rpc/server.d.ts +18 -0
  45. package/types/storage/index.d.ts +163 -0
  46. package/src/lib/base.js +0 -98
  47. package/src/lib/batch.js +0 -95
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -208
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. package/src/lib/room.js +0 -156
@@ -0,0 +1,232 @@
1
+ import sodium from 'sodium-universal'
2
+ import IdentityKey from 'keet-identity-key'
3
+ import bip39 from 'bip39-mnemonic'
4
+ import b4a from 'b4a'
5
+
6
+ import { CeroError } from '../lib/errors.js'
7
+ import { toId } from '../lib/utils.js'
8
+
9
+ const DERIVE_PATH = ['SLIP-0021', 'cero', 'v1', 'identity']
10
+
11
+ /**
12
+ * @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array, id: string }} KeyPair
13
+ * @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array, encryptionKey: Uint8Array, seed: Uint8Array }} IdentityFields
14
+ */
15
+
16
+ /**
17
+ * Long-lived user identity derived from a BIP-39 seed phrase. Holds the
18
+ * sign keypair, a symmetric encryption key and the canonical id/topic used
19
+ * by every other resource to identify this user.
20
+ */
21
+ export class Identity {
22
+ /** @param {IdentityFields} keys */
23
+ constructor(keys) {
24
+ /**
25
+ * Canonical z32-encoded public key.
26
+ *
27
+ * @type {string}
28
+ */
29
+ this.id = toId(keys.publicKey)
30
+ /** @type {Uint8Array} */
31
+ this.publicKey = keys.publicKey
32
+ /** @type {Uint8Array} */
33
+ this.secretKey = keys.secretKey
34
+ /**
35
+ * Symmetric key for encrypting per-identity payloads.
36
+ *
37
+ * @type {Uint8Array}
38
+ */
39
+ this.encryptionKey = keys.encryptionKey
40
+ /**
41
+ * Hash of the public key — used as the swarm topic.
42
+ *
43
+ * @type {Uint8Array}
44
+ */
45
+ this.topic = topicOf(keys.publicKey)
46
+ /**
47
+ * Original 16- or 32-byte entropy.
48
+ *
49
+ * @type {Uint8Array}
50
+ */
51
+ this.seed = keys.seed
52
+ Object.freeze(this)
53
+ }
54
+
55
+ /**
56
+ * Detached Ed25519 signature over `message`.
57
+ *
58
+ * @param {Uint8Array} message
59
+ * @returns {Uint8Array}
60
+ */
61
+ sign(message) {
62
+ const sig = b4a.alloc(sodium.crypto_sign_BYTES)
63
+ sodium.crypto_sign_detached(sig, message, this.secretKey)
64
+ return sig
65
+ }
66
+
67
+ /**
68
+ * Verify a signature against this identity's public key.
69
+ *
70
+ * @param {Uint8Array} message
71
+ * @param {Uint8Array} signature
72
+ * @returns {boolean}
73
+ */
74
+ verify(message, signature) {
75
+ return sodium.crypto_sign_verify_detached(signature, message, this.publicKey)
76
+ }
77
+
78
+ /**
79
+ * Render the underlying seed as a BIP-39 mnemonic phrase.
80
+ *
81
+ * @returns {string}
82
+ */
83
+ toPhrase() {
84
+ return bip39.entropyToMnemonic(this.seed)
85
+ }
86
+
87
+ /**
88
+ * Build an identity from 16- or 32-byte entropy.
89
+ *
90
+ * @param {Uint8Array} seed
91
+ * @returns {Promise<Identity>}
92
+ */
93
+ static async fromSeed(seed) {
94
+ if (!Identity.isSeed(seed)) throw CeroError.INVALID('seed must be a 16- or 32-byte buffer')
95
+ const mnemonic = bip39.entropyToMnemonic(seed)
96
+ const id = await IdentityKey.from({ mnemonic })
97
+ const { publicKey, secretKey } = id.identityKeyPair
98
+ const encryptionKey = id.keyChain.getSymmetricKey([...DERIVE_PATH, 'encryption'])
99
+ return new Identity({ publicKey, secretKey, encryptionKey, seed })
100
+ }
101
+
102
+ /**
103
+ * Build an identity from a BIP-39 mnemonic.
104
+ *
105
+ * @param {string} phrase
106
+ * @returns {Promise<Identity>}
107
+ */
108
+ static async fromPhrase(phrase) {
109
+ if (!Identity.isPhrase(phrase)) throw CeroError.INVALID('phrase must be a valid BIP39 mnemonic')
110
+ return Identity.fromSeed(bip39.mnemonicToEntropy(phrase))
111
+ }
112
+
113
+ /**
114
+ * Generate a fresh identity from CSPRNG entropy.
115
+ *
116
+ * @param {{ words?: 12 | 24 }} [opts]
117
+ * @returns {Promise<Identity>}
118
+ */
119
+ static async generate({ words = 12 } = {}) {
120
+ return Identity.fromSeed(Identity.randomSeed(words))
121
+ }
122
+
123
+ /**
124
+ * Generate a random BIP-39 mnemonic.
125
+ *
126
+ * @param {12 | 24} [words]
127
+ * @returns {string}
128
+ */
129
+ static genPhrase(words = 12) {
130
+ return bip39.entropyToMnemonic(Identity.randomSeed(words))
131
+ }
132
+
133
+ /**
134
+ * Phrase → seed entropy.
135
+ *
136
+ * @param {string} phrase
137
+ * @returns {Uint8Array}
138
+ */
139
+ static toSeed(phrase) {
140
+ return bip39.mnemonicToEntropy(phrase)
141
+ }
142
+
143
+ /**
144
+ * Seed entropy → phrase.
145
+ *
146
+ * @param {Uint8Array} seed
147
+ * @returns {string}
148
+ */
149
+ static toPhrase(seed) {
150
+ return bip39.entropyToMnemonic(seed)
151
+ }
152
+
153
+ /**
154
+ * Validate that a value is a 16- or 32-byte buffer.
155
+ *
156
+ * @param {any} x
157
+ * @returns {x is Uint8Array}
158
+ */
159
+ static isSeed(x) {
160
+ if (!b4a.isBuffer(x) && !(x instanceof Uint8Array)) return false
161
+ return x.length === 16 || x.length === 32
162
+ }
163
+
164
+ /**
165
+ * Validate that a string is a BIP-39 mnemonic.
166
+ *
167
+ * @param {unknown} x
168
+ * @returns {x is string}
169
+ */
170
+ static isPhrase(x) {
171
+ if (typeof x !== 'string') return false
172
+ try {
173
+ bip39.mnemonicToEntropy(x)
174
+ return true
175
+ } catch {
176
+ return false
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Verify a signature against an arbitrary public key.
182
+ *
183
+ * @param {Uint8Array} publicKey
184
+ * @param {Uint8Array} message
185
+ * @param {Uint8Array} signature
186
+ * @returns {boolean}
187
+ */
188
+ static verify(publicKey, message, signature) {
189
+ return sodium.crypto_sign_verify_detached(signature, message, publicKey)
190
+ }
191
+
192
+ /**
193
+ * Generate a fresh Ed25519 keypair (for devices, blind invites, etc.).
194
+ *
195
+ * @returns {KeyPair}
196
+ */
197
+ static randomKeyPair() {
198
+ const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
199
+ const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
200
+ sodium.crypto_sign_keypair(publicKey, secretKey)
201
+ return { publicKey, secretKey, id: toId(publicKey) }
202
+ }
203
+
204
+ /**
205
+ * N bytes from a CSPRNG.
206
+ *
207
+ * @param {number} [n]
208
+ * @returns {Uint8Array}
209
+ */
210
+ static randomBytes(n = 32) {
211
+ const buf = b4a.alloc(n)
212
+ sodium.randombytes_buf(buf)
213
+ return buf
214
+ }
215
+
216
+ /**
217
+ * Fresh seed entropy sized for the chosen mnemonic length.
218
+ *
219
+ * @param {12 | 24} [words]
220
+ * @returns {Uint8Array}
221
+ */
222
+ static randomSeed(words = 12) {
223
+ if (words !== 12 && words !== 24) throw new RangeError('words must be 12 or 24')
224
+ return Identity.randomBytes((words / 3) * 4)
225
+ }
226
+ }
227
+
228
+ function topicOf(publicKey) {
229
+ const topic = b4a.alloc(32)
230
+ sodium.crypto_generichash(topic, publicKey)
231
+ return topic
232
+ }
package/src/index.js CHANGED
@@ -1 +1,20 @@
1
- export * from './lib/index.js'
1
+ /**
2
+ * @cero-base/core — primitives for building p2p applications. This barrel
3
+ * re-exports the canonical surface: identity, storage, network, database,
4
+ * blobs, rpc, pairing, the schema DSL, the error class, and shared utils.
5
+ *
6
+ * Tree-shake-friendly: subpath imports (`@cero-base/core/identity`, etc.)
7
+ * are also available for consumers that need a smaller graph.
8
+ */
9
+
10
+ export { Identity } from './identity/index.js'
11
+ export { Storage } from './storage/index.js'
12
+ export { Network } from './network/index.js'
13
+ export { Database } from './database/index.js'
14
+ export { Blobs } from './blobs/index.js'
15
+ export { RPCServer, RPCClient, bindCodec } from './rpc/index.js'
16
+ export { Pairing } from './pairing/index.js'
17
+ export { Invite } from './pairing/invite.js'
18
+ export { t, schema } from './lib/schema.js'
19
+ export { genId, toId, toKey, isKeyId, subscribe } from './lib/utils.js'
20
+ export { CeroError } from './lib/errors.js'
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
@@ -1,4 +1,24 @@
1
- // User-facing scope names for cero-base schemas
2
- export const SCOPE_LOCAL = 'local'
3
- export const SCOPE_PRIVATE = 'private'
4
- export const SCOPE_SHARED = 'shared'
1
+ // Schema kinds
2
+ export const SINGLE = 'single'
3
+ export const COLLECTION = 'collection'
4
+ export const HANDLE = 'handle'
5
+ export const ACTION = 'action'
6
+
7
+ // Network topic modes
8
+ export const ACTIVE = 'active'
9
+ export const PASSIVE = 'passive'
10
+
11
+ // Storage backends
12
+ export const ROCKS = 'rocks'
13
+ export const BEE = 'bee'
14
+
15
+ // Roles
16
+ export const OWNER = 'owner'
17
+ export const WRITE = 'write'
18
+ export const READ = 'read'
19
+
20
+ // Identity / hypercore namespace
21
+ export const NAMESPACE = 'cero'
22
+
23
+ // Internal counter collection (assigns monotonic `index` to every row in every collection)
24
+ export const COUNTERS = 'counters'
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Canonical error class. Every instance carries a stable `code` field which
3
+ * is also prefixed to the message, so logs stay self-identifying. Static
4
+ * factories produce errors with the right code + a sensible message.
5
+ */
6
+ export class CeroError extends Error {
7
+ /**
8
+ * @param {string} code Stable identifier (e.g. `REQUIRED`, `EXPIRED`).
9
+ * @param {string} [message] Human-readable detail; appended after the code.
10
+ * @param {Record<string, unknown> | null} [extras] Extra fields copied onto the instance.
11
+ */
12
+ constructor(code, message, extras = null) {
13
+ super(message ? `${code}: ${message}` : code)
14
+ this.isCeroError = true
15
+ this.code = code
16
+ if (extras) Object.assign(this, extras)
17
+ if (Error.captureStackTrace) Error.captureStackTrace(this, CeroError)
18
+ }
19
+
20
+ get name() {
21
+ return 'CeroError'
22
+ }
23
+
24
+ /**
25
+ * Type-guard for `err instanceof CeroError` that survives realm boundaries.
26
+ *
27
+ * @param {any} err
28
+ * @returns {err is CeroError}
29
+ */
30
+ static isCeroError(err) {
31
+ return err?.isCeroError === true
32
+ }
33
+
34
+ /**
35
+ * Missing required argument.
36
+ *
37
+ * @param {string} name
38
+ */
39
+ static REQUIRED(name) {
40
+ return new CeroError('REQUIRED', `${name} is required`)
41
+ }
42
+ /**
43
+ * Argument failed validation.
44
+ *
45
+ * @param {string} msg
46
+ */
47
+ static INVALID(msg) {
48
+ return new CeroError('INVALID', msg)
49
+ }
50
+ /**
51
+ * Operation on a closed resource.
52
+ *
53
+ * @param {string} resource
54
+ */
55
+ static CLOSED(resource) {
56
+ return new CeroError('CLOSED', `${resource} is closed`)
57
+ }
58
+ /**
59
+ * Operation before `ready()` resolved.
60
+ *
61
+ * @param {string} resource
62
+ * @param {string} name
63
+ */
64
+ static NOT_READY(resource, name) {
65
+ return new CeroError('NOT_READY', `${resource} is not ready — await ${name}.ready() first`)
66
+ }
67
+ /**
68
+ * Resource has been destroyed.
69
+ *
70
+ * @param {string} resource
71
+ */
72
+ static DESTROYED(resource) {
73
+ return new CeroError('DESTROYED', `${resource} is destroyed`)
74
+ }
75
+ /**
76
+ * Lookup failed for a typed id.
77
+ *
78
+ * @param {string} kind
79
+ * @param {string} id
80
+ */
81
+ static UNKNOWN(kind, id) {
82
+ return new CeroError('UNKNOWN', `unknown ${kind}: ${id}`)
83
+ }
84
+ /**
85
+ * Write attempted on a read-only handle.
86
+ *
87
+ * @param {string} resource
88
+ */
89
+ static NOT_WRITABLE(resource) {
90
+ return new CeroError('NOT_WRITABLE', `${resource} is not writable`)
91
+ }
92
+ /**
93
+ * Async operation exceeded its deadline.
94
+ *
95
+ * @param {string} what
96
+ */
97
+ static TIMED_OUT(what) {
98
+ return new CeroError('TIMED_OUT', `${what} timed out`)
99
+ }
100
+ /**
101
+ * Feature is not yet implemented.
102
+ *
103
+ * @param {string} what
104
+ */
105
+ static UNSUPPORTED(what) {
106
+ return new CeroError('UNSUPPORTED', `${what} not yet supported`)
107
+ }
108
+
109
+ /**
110
+ * Invite payload failed to parse or verify.
111
+ *
112
+ * @param {string} [msg]
113
+ */
114
+ static INVALID_INVITE(msg = 'invalid invite') {
115
+ return new CeroError('INVALID_INVITE', msg)
116
+ }
117
+ /**
118
+ * Invite is past its TTL.
119
+ *
120
+ * @param {string} [msg]
121
+ */
122
+ static EXPIRED(msg = 'invite expired') {
123
+ return new CeroError('EXPIRED', msg)
124
+ }
125
+ /**
126
+ * Host refused the join — `reason` is exposed on the instance.
127
+ *
128
+ * @param {string | null} [reason]
129
+ * @param {string} [msg]
130
+ */
131
+ static DENIED(reason = null, msg = 'pairing denied') {
132
+ return new CeroError('DENIED', msg, { reason })
133
+ }
134
+ /**
135
+ * Pairing handshake did not complete in time.
136
+ *
137
+ * @param {string} [msg]
138
+ */
139
+ static TIMEOUT(msg = 'pairing timed out') {
140
+ return new CeroError('TIMEOUT', msg)
141
+ }
142
+ /**
143
+ * Underlying swarm/transport failure.
144
+ *
145
+ * @param {string} [msg]
146
+ */
147
+ static NETWORK_ERROR(msg = 'network error') {
148
+ return new CeroError('NETWORK_ERROR', msg)
149
+ }
150
+ }
package/src/lib/schema.js CHANGED
@@ -1,203 +1,68 @@
1
- import { singular, toFields, scopeNs, isLocal, isPrivate, isShared } from './utils.js'
2
- import { SCOPE_LOCAL, SCOPE_PRIVATE, SCOPE_SHARED } from './constants.js'
1
+ import { CeroError } from './errors.js'
2
+ import { SINGLE, COLLECTION, ACTION } from './constants.js'
3
3
 
4
4
  /**
5
- * Type helpers for defining schema fields
6
- * Usage: { name: t.string, count: t.int, active: t.bool }
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
7
9
  */
8
- export const t = {
9
- string: { type: 'string', required: true },
10
- int: { type: 'int', required: true },
11
- uint: { type: 'uint', required: true },
12
- float: { type: 'float', required: true },
13
- bool: { type: 'bool', required: true },
14
- buffer: { type: 'buffer', required: true },
15
- fixed32: { type: 'fixed32', required: true },
16
- json: { type: 'json', required: true },
17
10
 
18
- optional: (field) => ({ ...field, required: false }),
19
- array: (field) => ({ ...field, array: true }),
20
- ref: (collection) => ({ type: 'string', required: true, ref: collection })
21
- }
11
+ /** @param {string} name @returns {Prim} */
12
+ const prim = (name) => ({ prim: name })
22
13
 
23
- function schemaFields(def) {
24
- const fields = toFields(def.fields)
25
- // every type gets index for range queries / pagination (assigned by cero's Base)
26
- fields.push({ name: 'index', type: 'uint', required: false })
27
- if (def.timestamps) {
28
- fields.push({ name: 'createdAt', type: 'uint', required: true })
29
- fields.push({ name: 'updatedAt', type: 'uint', required: false })
14
+ /**
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.
17
+ */
18
+ export const t = {
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 }
30
56
  }
31
- return fields
32
- }
33
-
34
- function inputFields(def) {
35
- return toFields(def.fields).map((f) => ({ ...f, required: false }))
36
- }
37
-
38
- function delFields(def) {
39
- const keys = {}
40
- for (const k of def.key || ['id']) keys[k] = def.fields[k]
41
- return toFields(keys)
42
57
  }
43
58
 
44
- t.schema = defineSchema
45
-
46
- export function defineSchema(collections, opts = {}) {
47
- const entries = Object.entries(collections)
48
- const types = new Map(entries.map(([name]) => [name, singular(name)]))
49
-
50
- const getScope = ([, def]) => def.scope || SCOPE_PRIVATE
51
- const localEntries = entries.filter((e) => getScope(e) === SCOPE_LOCAL)
52
- const privateEntries = entries.filter((e) => getScope(e) === SCOPE_PRIVATE)
53
- const sharedEntries = entries.filter((e) => getScope(e) === SCOPE_SHARED)
54
-
55
- const nsFor = (scope) => (scope === SCOPE_SHARED && opts.namespace) || scopeNs(scope)
56
-
57
- return {
58
- namespace: opts.namespace || null,
59
- collections,
60
- types,
61
-
62
- getScope(collection) {
63
- const def = collections[collection]
64
- return def ? def.scope || SCOPE_PRIVATE : null
65
- },
66
-
67
- isLocal(collection) {
68
- return isLocal(this.getScope(collection))
69
- },
70
- isPrivate(collection) {
71
- return isPrivate(this.getScope(collection))
72
- },
73
- isShared(collection) {
74
- return isShared(this.getScope(collection))
75
- },
76
-
77
- hasTimestamps(collection) {
78
- return collections[collection]?.timestamps === true
79
- },
80
-
81
- hasCounter(collection) {
82
- return collections[collection]?.counter === true
83
- },
84
-
85
- counterNames(scope) {
86
- const items =
87
- scope === SCOPE_SHARED
88
- ? sharedEntries
89
- : scope === SCOPE_LOCAL
90
- ? localEntries
91
- : privateEntries
92
- // map: type name (singular) → collection name (plural)
93
- const map = {}
94
- for (const [name, def] of items) {
95
- if (def.counter) map[types.get(name)] = name
96
- }
97
- return map
98
- },
99
-
100
- resolve(collection, prefix) {
101
- const scope = this.getScope(collection)
102
- const type = types.get(collection)
103
- const name = prefix ? `${prefix}-${type}` : type
104
- return { spec: scopeNs(scope), path: `@${nsFor(scope)}/${name}` }
105
- },
106
-
107
- require(collection, context) {
108
- const scope = this.getScope(collection)
109
- if (!scope) throw new Error(`Unknown collection: '${collection}'`)
110
- if (context === 'db' && scope === SCOPE_SHARED) {
111
- throw new Error(`'${collection}' is shared — use room.collection('${collection}')`)
112
- }
113
- if (context === 'room' && scope !== SCOPE_SHARED) {
114
- throw new Error(`'${collection}' is ${scope} — use db.collection('${collection}')`)
115
- }
116
- return scope
117
- },
118
-
119
- getPath(collection) {
120
- const scope = this.getScope(collection)
121
- return `@${nsFor(scope)}/${collection}`
122
- },
123
-
124
- build(scope, register, nsName) {
125
- const items =
126
- scope === SCOPE_SHARED
127
- ? sharedEntries
128
- : scope === SCOPE_LOCAL
129
- ? localEntries
130
- : privateEntries
131
- const local = scope === SCOPE_LOCAL
132
- const ns = nsName || scopeNs(scope)
133
- for (const [name, def] of items) {
134
- const type = types.get(name)
135
- const fields = schemaFields(def)
136
- const typePath = `@${ns}/${type}`
137
-
138
- register.type({ name: type, compact: false, fields })
139
- register.type({ name: `input-${type}`, compact: false, fields: inputFields(def) })
140
- if (!local) register.type({ name: `del-${type}`, compact: false, fields: delFields(def) })
141
-
142
- register.collection({
143
- name,
144
- schema: typePath,
145
- key: def.key || ['id'],
146
- counter: !!def.counter
147
- })
148
-
149
- if (def.indexes) {
150
- for (const [idx, idxFields] of Object.entries(def.indexes)) {
151
- register.index({ name: `${name}-${idx}`, collection: `@${ns}/${name}`, key: idxFields })
152
- }
153
- }
154
-
155
- if (!local) {
156
- register.dispatch({ name: `add-${type}`, requestType: typePath })
157
- register.dispatch({ name: `set-${type}`, requestType: typePath })
158
- register.dispatch({ name: `del-${type}`, requestType: `@${ns}/del-${type}` })
159
- }
160
- }
161
- },
162
-
163
- routes(scope) {
164
- const items = scope === SCOPE_PRIVATE ? privateEntries : sharedEntries
165
- const handlers = {}
166
- const p = (name) => `@${nsFor(scope)}/${name}`
167
- for (const [name, def] of items) {
168
- const type = types.get(name)
169
- const path = p(name)
170
- const keys = def.key || ['id']
171
-
172
- handlers[p(`add-${type}`)] = async (op, ctx) => {
173
- const counterPath = p('counters')
174
- const counter = (await ctx.view.get(counterPath, { name })) || { name, value: 0 }
175
- op.index = counter.value
176
- counter.value++
177
- await ctx.view.insert(counterPath, counter)
178
- await ctx.view.insert(path, op)
179
- }
180
-
181
- handlers[p(`set-${type}`)] = (op, ctx) => ctx.view.insert(path, op)
182
- handlers[p(`del-${type}`)] = async (op, ctx) => {
183
- await ctx.view.delete(
184
- path,
185
- keys.reduce((acc, k) => ((acc[k] = op[k]), acc), {})
186
- )
187
- const counterPath = p('counters')
188
- const counter = (await ctx.view.get(counterPath, { name })) || { name, value: 0 }
189
- counter.value = Math.max(0, counter.value - 1)
190
- await ctx.view.insert(counterPath, counter)
191
- }
192
-
193
- if (def.handlers) {
194
- for (const [op, handler] of Object.entries(def.handlers)) {
195
- handlers[p(op)] = handler
196
- }
197
- }
198
- }
199
-
200
- return handlers
201
- }
202
- }
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 }
203
68
  }