nervur 0.8.0 → 0.8.2
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 +56 -0
- package/blueprint.mjs +73 -9
- package/ground.mjs +3 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -313,6 +313,62 @@ MEMBER — once, and never again. `Clock` and `Entropy` are not floor: they
|
|
|
313
313
|
are ordinary modules a program declares, pinnable so a program never
|
|
314
314
|
reaches for the wall.
|
|
315
315
|
|
|
316
|
+
## Extending the administration
|
|
317
|
+
|
|
318
|
+
A ground's administration is a being like any other, and the only one whose
|
|
319
|
+
context holds the registry — so a door that must create, upgrade or destroy
|
|
320
|
+
beings belongs on it and nowhere else. Adopters extend it; nobody replaces
|
|
321
|
+
it.
|
|
322
|
+
|
|
323
|
+
Pass an extension through the `blueprints` module and the ground composes it
|
|
324
|
+
with the standard administration. Write it as an ordinary blueprint whose
|
|
325
|
+
schema **extends** the roots:
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
const cases = `({
|
|
329
|
+
name: 'procese.cases',
|
|
330
|
+
needs: { caller: true, modules: [{ module: 'memory', contract: 'nervur.memory' }] },
|
|
331
|
+
memory: { 'case/*': { type: 'String', class: 'data' } },
|
|
332
|
+
interface: \`
|
|
333
|
+
extend type Mutation {
|
|
334
|
+
openCase(title: String!): String @rights(is: [SELF, MEMBER])
|
|
335
|
+
}
|
|
336
|
+
\`,
|
|
337
|
+
resolvers: {
|
|
338
|
+
Mutation: {
|
|
339
|
+
openCase: (_, { title }, { from, modules, registry }) => {
|
|
340
|
+
const at = registry.create({ program: CASE, refs: [{ address: from }] });
|
|
341
|
+
modules.memory.write('case/' + at, title);
|
|
342
|
+
return at ?? null;
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
})`;
|
|
347
|
+
|
|
348
|
+
const ground = Ground({ cells, keychain, blueprints: { ground: () => cases } });
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The composed program is one blueprint with one digest, so `describe` answers
|
|
352
|
+
what this ground's administration actually is, and an outsider can pin it.
|
|
353
|
+
|
|
354
|
+
Three rules hold, and each refuses the whole ground rather than bending:
|
|
355
|
+
|
|
356
|
+
1. **Standard door names are reserved.** Declare `create`, `census`,
|
|
357
|
+
`upgrade`, `destroy`, `alias`, `attest`, `opened` or `init` and no ground
|
|
358
|
+
rises. You cannot change what a standard door means — which is why a
|
|
359
|
+
stranger may trust that if `create` answers, it created.
|
|
360
|
+
2. **Add only.** Use `extend type Query` / `extend type Mutation`; cell
|
|
361
|
+
names, module slots and `needs.config` may not collide with the
|
|
362
|
+
administration's own.
|
|
363
|
+
3. **Close a door the way anything is closed here** — fence it behind a
|
|
364
|
+
right the ground never grants. The door stands, means what the
|
|
365
|
+
constitution says, and answers nobody: that is how an appliance ground
|
|
366
|
+
stops creating beings, without redefining a thing.
|
|
367
|
+
|
|
368
|
+
The acts stay the core's own: an extension reaches them through the same
|
|
369
|
+
registry face the standard doors use, so it can invent no power the standard
|
|
370
|
+
administration lacked.
|
|
371
|
+
|
|
316
372
|
## Writing a module
|
|
317
373
|
|
|
318
374
|
Five rules, each load-bearing:
|
package/blueprint.mjs
CHANGED
|
@@ -80,6 +80,9 @@ const RESERVED = ['eval', 'import', '__proto__', 'prototype', 'Math.random'];
|
|
|
80
80
|
|
|
81
81
|
const SHADOWED = FORBIDDEN.map(([name]) => name).filter((name) => !RESERVED.includes(name));
|
|
82
82
|
|
|
83
|
+
const doorHolder = (one) =>
|
|
84
|
+
one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION;
|
|
85
|
+
|
|
83
86
|
const RIGHTS_DIRECTIVE = 'directive @rights(is: [Rights!]!) on FIELD_DEFINITION';
|
|
84
87
|
|
|
85
88
|
const DEFAULT_RIGHTS = `enum Rights { SELF MEMBER STRANGER }`;
|
|
@@ -156,6 +159,67 @@ const resolverSources = (resolvers) =>
|
|
|
156
159
|
.map((field) => `${type}.${field}=${resolvers[type][field].toString()}`),
|
|
157
160
|
);
|
|
158
161
|
|
|
162
|
+
const merged = (base, added) =>
|
|
163
|
+
Object.fromEntries(
|
|
164
|
+
[...new Set([...Object.keys(base ?? {}), ...Object.keys(added ?? {})])].map((key) => [
|
|
165
|
+
key,
|
|
166
|
+
{ ...(base ?? {})[key], ...(added ?? {})[key] },
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const doorsIn = (sdl) =>
|
|
171
|
+
parse(sdl)
|
|
172
|
+
.definitions.filter(
|
|
173
|
+
(one) => one.kind === Kind.OBJECT_TYPE_DEFINITION || one.kind === Kind.OBJECT_TYPE_EXTENSION,
|
|
174
|
+
)
|
|
175
|
+
.flatMap((one) => (one.fields ?? []).map((field) => `${one.name.value}.${field.name.value}`));
|
|
176
|
+
|
|
177
|
+
const composed = (baseSource, addedSource) => {
|
|
178
|
+
try {
|
|
179
|
+
const base = evaluated(baseSource);
|
|
180
|
+
const added = evaluated(addedSource);
|
|
181
|
+
const standard = doorsIn(base.interface);
|
|
182
|
+
if (doorsIn(added.interface).some((one) => standard.includes(one))) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const cells = { ...(base.memory ?? {}), ...(added.memory ?? {}) };
|
|
186
|
+
if (
|
|
187
|
+
Object.keys(base.memory ?? {}).some((key) => (added.memory ?? {})[key] !== undefined) ||
|
|
188
|
+
(added.needs?.config !== undefined && base.needs?.config !== undefined)
|
|
189
|
+
) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const slots = [...(base.needs?.modules ?? []), ...(added.needs?.modules ?? [])];
|
|
193
|
+
if (new Set(slots.map((one) => one.module)).size !== slots.length) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const needs = {
|
|
197
|
+
...(base.needs ?? {}),
|
|
198
|
+
...(added.needs ?? {}),
|
|
199
|
+
caller: base.needs?.caller === true || added.needs?.caller === true,
|
|
200
|
+
modules: slots,
|
|
201
|
+
};
|
|
202
|
+
const doors = merged(base.resolvers, added.resolvers);
|
|
203
|
+
const written = Object.entries(doors)
|
|
204
|
+
.map(
|
|
205
|
+
([type, fields]) =>
|
|
206
|
+
`${type}: { ${Object.entries(fields)
|
|
207
|
+
.map(([field, one]) => `${field}: ${one.toString()}`)
|
|
208
|
+
.join(', ')} }`,
|
|
209
|
+
)
|
|
210
|
+
.join(', ');
|
|
211
|
+
return `({
|
|
212
|
+
name: ${JSON.stringify(added.name)},
|
|
213
|
+
needs: ${JSON.stringify(needs)},
|
|
214
|
+
memory: ${JSON.stringify(cells)},
|
|
215
|
+
interface: ${JSON.stringify(`${base.interface}\n${added.interface}`)},
|
|
216
|
+
resolvers: { ${written} },
|
|
217
|
+
})`;
|
|
218
|
+
} catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
159
223
|
const inputCheck = (schema, typeName, given) => {
|
|
160
224
|
const type = schema.getType(typeName);
|
|
161
225
|
if (type?.getFields === undefined) {
|
|
@@ -189,10 +253,10 @@ const Blueprint = (source) => {
|
|
|
189
253
|
];
|
|
190
254
|
|
|
191
255
|
const admits = (typeName, fieldName, held) => {
|
|
192
|
-
const
|
|
193
|
-
(one) => one
|
|
194
|
-
|
|
195
|
-
|
|
256
|
+
const field = document.definitions
|
|
257
|
+
.filter((one) => doorHolder(one) && one.name.value === typeName)
|
|
258
|
+
.flatMap((one) => one.fields ?? [])
|
|
259
|
+
.find((one) => one.name.value === fieldName);
|
|
196
260
|
const allowed = field === undefined ? undefined : rightsOf(field);
|
|
197
261
|
return allowed === undefined ? true : allowed.includes(held);
|
|
198
262
|
};
|
|
@@ -212,7 +276,7 @@ const Blueprint = (source) => {
|
|
|
212
276
|
schema.getSubscriptionType()?.name,
|
|
213
277
|
].filter((one) => one !== undefined);
|
|
214
278
|
const undeclared = document.definitions
|
|
215
|
-
.filter((one) => one
|
|
279
|
+
.filter((one) => doorHolder(one) && roots.includes(one.name.value))
|
|
216
280
|
.flatMap((one) => one.fields ?? [])
|
|
217
281
|
.filter((one) => {
|
|
218
282
|
const allowed = rightsOf(one);
|
|
@@ -260,17 +324,17 @@ const Blueprint = (source) => {
|
|
|
260
324
|
...document,
|
|
261
325
|
definitions: document.definitions
|
|
262
326
|
.map((one) =>
|
|
263
|
-
one
|
|
327
|
+
doorHolder(one)
|
|
264
328
|
? {
|
|
265
329
|
...one,
|
|
266
|
-
fields: one.fields.filter((field) => {
|
|
330
|
+
fields: (one.fields ?? []).filter((field) => {
|
|
267
331
|
const allowed = rightsOf(field);
|
|
268
332
|
return allowed === undefined || allowed.includes(held);
|
|
269
333
|
}),
|
|
270
334
|
}
|
|
271
335
|
: one,
|
|
272
336
|
)
|
|
273
|
-
.filter((one) => one
|
|
337
|
+
.filter((one) => !doorHolder(one) || one.fields.length > 0),
|
|
274
338
|
}),
|
|
275
339
|
trespass: [],
|
|
276
340
|
});
|
|
@@ -279,4 +343,4 @@ const Blueprint = (source) => {
|
|
|
279
343
|
}
|
|
280
344
|
};
|
|
281
345
|
|
|
282
|
-
export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, coreClass, reads };
|
|
346
|
+
export { Blueprint, CLASSES, CORE_ONLY, FORBIDDEN, OPEN, STRUCTURAL, composed, coreClass, reads };
|
package/ground.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ADMIN } from './admin.mjs';
|
|
2
2
|
import { Being, Compartment, Registry, make, rootFor } from './being.mjs';
|
|
3
|
-
import { Blueprint, coreClass } from './blueprint.mjs';
|
|
3
|
+
import { Blueprint, composed, coreClass } from './blueprint.mjs';
|
|
4
4
|
import { Client } from './client.mjs';
|
|
5
5
|
import { Contracts } from './contract.mjs';
|
|
6
6
|
import { Memory, Paths, Wire } from './modules.mjs';
|
|
@@ -13,22 +13,6 @@ const GROUND = 'Ground';
|
|
|
13
13
|
|
|
14
14
|
const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
|
|
15
15
|
|
|
16
|
-
const doorsOf = (source) => {
|
|
17
|
-
const written = Blueprint(source);
|
|
18
|
-
if (written === undefined) {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
return [written.schema.getQueryType(), written.schema.getMutationType()]
|
|
22
|
-
.filter((one) => one !== undefined && one !== null)
|
|
23
|
-
.flatMap((one) => Object.keys(one.getFields()));
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const redefines = (source) => {
|
|
27
|
-
const custom = doorsOf(source);
|
|
28
|
-
const standard = doorsOf(ADMIN);
|
|
29
|
-
return custom === undefined || custom.some((one) => standard.includes(one));
|
|
30
|
-
};
|
|
31
|
-
|
|
32
16
|
const install = ({ cells, keychain, program }) => {
|
|
33
17
|
const memory = Memory(cells);
|
|
34
18
|
const voice = Voice();
|
|
@@ -58,10 +42,10 @@ const Ground = (modules = {}) => {
|
|
|
58
42
|
}
|
|
59
43
|
|
|
60
44
|
const extension = modules.blueprints?.ground();
|
|
61
|
-
|
|
45
|
+
const program = extension === undefined ? ADMIN : composed(ADMIN, extension);
|
|
46
|
+
if (program === undefined || Blueprint(program)?.digest === undefined) {
|
|
62
47
|
return undefined;
|
|
63
48
|
}
|
|
64
|
-
const program = extension ?? ADMIN;
|
|
65
49
|
if (cells.peek('ground/admin') === undefined) {
|
|
66
50
|
install({ cells, keychain, program });
|
|
67
51
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nervur",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "A distributed system of sovereign beings
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "A distributed system of sovereign beings \u2014 one door, one question: by whose authority.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Razvan Gherghina",
|
|
7
7
|
"homepage": "https://nervur.org",
|