@brydio/manifest 0.1.0-alpha.37 → 0.1.0-alpha.39
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/package.json +2 -2
- package/src/field-types.d.ts +6 -0
- package/src/field-types.js +3 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/readers.d.ts +54 -0
- package/src/readers.js +96 -0
- package/src/schema.d.ts +21 -6
- package/src/schema.js +31 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brydio/manifest",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.39",
|
|
4
4
|
"description": "The shape of a Brydio app's manifest, its collections' schemas, and the checks that say what is wrong with one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "https://github.com/nakel-ola/brydio-sdk",
|
|
11
11
|
"directory": "packages/manifest"
|
|
12
12
|
},
|
|
13
|
-
"gitHead": "
|
|
13
|
+
"gitHead": "4b20d567d9361683e104ec609d231111bd93bcbf",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"types": "./src/index.d.ts",
|
package/src/field-types.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface FieldType {
|
|
|
40
40
|
* row's table. Set by the host only, never parsed from a manifest.
|
|
41
41
|
*/
|
|
42
42
|
plain?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The most entries a `string[]` holds, when not `FIELD_LIMITS.listEntries`:
|
|
45
|
+
* a collection's readers or editors field takes `READERS_LIMIT` (P16,
|
|
46
|
+
* DW06). Set by the host only, never parsed from a manifest.
|
|
47
|
+
*/
|
|
48
|
+
maxEntries?: number;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* The bounds a schema and a record are held to (A3-F01-S03).
|
package/src/field-types.js
CHANGED
|
@@ -259,7 +259,7 @@ export function valueSchema(type) {
|
|
|
259
259
|
case 'boolean':
|
|
260
260
|
return z.boolean();
|
|
261
261
|
case 'string[]':
|
|
262
|
-
return z.array(z.string().max(FIELD_LIMITS.stringChars)).max(FIELD_LIMITS.listEntries);
|
|
262
|
+
return z.array(z.string().max(FIELD_LIMITS.stringChars)).max(type.maxEntries ?? FIELD_LIMITS.listEntries);
|
|
263
263
|
case 'canvas':
|
|
264
264
|
return z.object({ elements: z.number().int().min(0), text: z.string().max(CANVAS_SUMMARY_CHARS) }).strict();
|
|
265
265
|
}
|
|
@@ -294,8 +294,8 @@ export function valueProblem(field, type, value) {
|
|
|
294
294
|
case 'boolean':
|
|
295
295
|
return `${field} must be true or false.`;
|
|
296
296
|
case 'string[]':
|
|
297
|
-
return Array.isArray(value) && value.length > FIELD_LIMITS.listEntries
|
|
298
|
-
? `${field} may hold at most ${FIELD_LIMITS.listEntries} entries.`
|
|
297
|
+
return Array.isArray(value) && value.length > (type.maxEntries ?? FIELD_LIMITS.listEntries)
|
|
298
|
+
? `${field} may hold at most ${type.maxEntries ?? FIELD_LIMITS.listEntries} entries.`
|
|
299
299
|
: `${field} must be a list of short texts, each at most ${FIELD_LIMITS.stringChars.toLocaleString('en-GB')} characters.`;
|
|
300
300
|
case 'canvas':
|
|
301
301
|
return `${field} is a drawing: it is changed on its board, never written.`;
|
package/src/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, collectio
|
|
|
11
11
|
export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, type Moved, type OpenField, type OpenFieldChange, type OpenFieldValue, type OpenProblem, type OpenProblemCode, type OpenSchemaFlag, type OpenType, } from './open-schema.js';
|
|
12
12
|
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, type ConfirmEmailDeclared, type ConfirmEmailSpec, type ConfirmProblemCode, } from './confirm-email.js';
|
|
13
13
|
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, type AnonymousDeclared, type AnonymousProblemCode, type AnonymousSpec, } from './anonymous.js';
|
|
14
|
+
export { editorsOf, editorsProblems, READERS_LIMIT, readersOf, readersProblems, type EditorsProblemCode, type ReadersProblemCode } from './readers.js';
|
|
14
15
|
export { DOCUMENT_LIMITS } from './document-limits.js';
|
|
15
16
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
|
|
16
17
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, type Migration, type MigrationCode, type MigrationProblem, type MigrationStep, type SchemaChange, } from './migrations.js';
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, collectio
|
|
|
11
11
|
export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, } from "./open-schema.js";
|
|
12
12
|
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, } from "./confirm-email.js";
|
|
13
13
|
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, } from "./anonymous.js";
|
|
14
|
+
export { editorsOf, editorsProblems, READERS_LIMIT, readersOf, readersProblems } from "./readers.js";
|
|
14
15
|
export { DOCUMENT_LIMITS } from "./document-limits.js";
|
|
15
16
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
|
|
16
17
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
|
package/src/readers.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Record readers (P16): a record only the people it
|
|
4
|
+
* names may read.
|
|
5
|
+
*
|
|
6
|
+
* An app names one of the collection's own `string[]` fields, which holds
|
|
7
|
+
* user ids. When a record's list is empty, everyone who can read the
|
|
8
|
+
* instance reads it, as before; when it names anyone, nobody else finds it —
|
|
9
|
+
* not on a screen, not through a tool, not through the assistant, not on the
|
|
10
|
+
* co-edit socket. There is no admin past it: an app that wants its admins
|
|
11
|
+
* in puts them in the list. Brydio keeps the list in plain beside the record
|
|
12
|
+
* (`app_document.readers`), so every read can be narrowed before anything
|
|
13
|
+
* is decrypted. Mirrors Brydio's `apps/api/src/apps/manifest/readers.ts`;
|
|
14
|
+
* the two change together.
|
|
15
|
+
*/
|
|
16
|
+
export declare const readersSchema: z.ZodString;
|
|
17
|
+
export type ReadersProblemCode = 'data_readers_field' | 'data_readers_anonymous';
|
|
18
|
+
/** The most people one record's list may name. */
|
|
19
|
+
export declare const READERS_LIMIT = 1000;
|
|
20
|
+
export declare function readersProblems(collection: string, declared: {
|
|
21
|
+
schema: Record<string, unknown>;
|
|
22
|
+
readers?: string;
|
|
23
|
+
anonymous?: unknown;
|
|
24
|
+
}): {
|
|
25
|
+
code: ReadersProblemCode;
|
|
26
|
+
collection: string;
|
|
27
|
+
field?: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}[];
|
|
30
|
+
/** The plain list a record is kept with: its readers, or null for everyone who can read the instance. */
|
|
31
|
+
export declare function readersOf(field: string | undefined, body: Record<string, unknown>): string[] | null;
|
|
32
|
+
/**
|
|
33
|
+
* Record editors (DW06): a record only the people it names may change. The
|
|
34
|
+
* same shape as readers: one of the collection's own `string[]` fields of
|
|
35
|
+
* user ids, kept in plain beside the record (`app_document.editors`). Empty,
|
|
36
|
+
* and anyone who may write the instance may change it; naming anyone, and
|
|
37
|
+
* only they may — through a tool, a handler, the assistant or the co-edit
|
|
38
|
+
* socket, where anyone else's connection is read-only. Everyone who may read
|
|
39
|
+
* the record still reads it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const editorsSchema: z.ZodString;
|
|
42
|
+
export type EditorsProblemCode = 'data_editors_field' | 'data_editors_anonymous';
|
|
43
|
+
export declare function editorsProblems(collection: string, declared: {
|
|
44
|
+
schema: Record<string, unknown>;
|
|
45
|
+
editors?: string;
|
|
46
|
+
anonymous?: unknown;
|
|
47
|
+
}): {
|
|
48
|
+
code: EditorsProblemCode;
|
|
49
|
+
collection: string;
|
|
50
|
+
field?: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}[];
|
|
53
|
+
/** The plain list a record is kept with: its editors, or null for anyone who may write the instance. */
|
|
54
|
+
export declare const editorsOf: typeof readersOf;
|
package/src/readers.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FIELD_LIMITS, parseFieldType } from "./field-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Record readers (P16): a record only the people it
|
|
5
|
+
* names may read.
|
|
6
|
+
*
|
|
7
|
+
* An app names one of the collection's own `string[]` fields, which holds
|
|
8
|
+
* user ids. When a record's list is empty, everyone who can read the
|
|
9
|
+
* instance reads it, as before; when it names anyone, nobody else finds it —
|
|
10
|
+
* not on a screen, not through a tool, not through the assistant, not on the
|
|
11
|
+
* co-edit socket. There is no admin past it: an app that wants its admins
|
|
12
|
+
* in puts them in the list. Brydio keeps the list in plain beside the record
|
|
13
|
+
* (`app_document.readers`), so every read can be narrowed before anything
|
|
14
|
+
* is decrypted. Mirrors Brydio's `apps/api/src/apps/manifest/readers.ts`;
|
|
15
|
+
* the two change together.
|
|
16
|
+
*/
|
|
17
|
+
export const readersSchema = z.string().min(1).max(FIELD_LIMITS.nameChars);
|
|
18
|
+
/** The most people one record's list may name. */
|
|
19
|
+
export const READERS_LIMIT = 1_000;
|
|
20
|
+
export function readersProblems(collection, declared) {
|
|
21
|
+
if (!declared.readers)
|
|
22
|
+
return [];
|
|
23
|
+
const problems = [];
|
|
24
|
+
const raw = declared.schema[declared.readers];
|
|
25
|
+
let listed = false;
|
|
26
|
+
try {
|
|
27
|
+
listed = raw !== undefined && parseFieldType(raw).kind === 'string[]';
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Refused as a field type already.
|
|
31
|
+
}
|
|
32
|
+
if (!listed) {
|
|
33
|
+
problems.push({
|
|
34
|
+
code: 'data_readers_field',
|
|
35
|
+
collection,
|
|
36
|
+
field: declared.readers,
|
|
37
|
+
message: `"${collection}" names "${declared.readers}" as its readers, which must be one of its own string[] fields holding user ids.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// An anonymous answer names nobody; a list of who may read it would.
|
|
41
|
+
if (declared.anonymous) {
|
|
42
|
+
problems.push({
|
|
43
|
+
code: 'data_readers_anonymous',
|
|
44
|
+
collection,
|
|
45
|
+
message: `"${collection}" can't be anonymous and name its readers.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return problems;
|
|
49
|
+
}
|
|
50
|
+
/** The plain list a record is kept with: its readers, or null for everyone who can read the instance. */
|
|
51
|
+
export function readersOf(field, body) {
|
|
52
|
+
if (!field)
|
|
53
|
+
return null;
|
|
54
|
+
const value = body[field];
|
|
55
|
+
if (!Array.isArray(value))
|
|
56
|
+
return null;
|
|
57
|
+
const ids = [...new Set(value.filter((one) => typeof one === 'string' && one.length > 0))];
|
|
58
|
+
return ids.length ? ids.sort() : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Record editors (DW06): a record only the people it names may change. The
|
|
62
|
+
* same shape as readers: one of the collection's own `string[]` fields of
|
|
63
|
+
* user ids, kept in plain beside the record (`app_document.editors`). Empty,
|
|
64
|
+
* and anyone who may write the instance may change it; naming anyone, and
|
|
65
|
+
* only they may — through a tool, a handler, the assistant or the co-edit
|
|
66
|
+
* socket, where anyone else's connection is read-only. Everyone who may read
|
|
67
|
+
* the record still reads it.
|
|
68
|
+
*/
|
|
69
|
+
export const editorsSchema = readersSchema;
|
|
70
|
+
export function editorsProblems(collection, declared) {
|
|
71
|
+
if (!declared.editors)
|
|
72
|
+
return [];
|
|
73
|
+
const problems = [];
|
|
74
|
+
const raw = declared.schema[declared.editors];
|
|
75
|
+
let listed = false;
|
|
76
|
+
try {
|
|
77
|
+
listed = raw !== undefined && parseFieldType(raw).kind === 'string[]';
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Refused as a field type already.
|
|
81
|
+
}
|
|
82
|
+
if (!listed) {
|
|
83
|
+
problems.push({
|
|
84
|
+
code: 'data_editors_field',
|
|
85
|
+
collection,
|
|
86
|
+
field: declared.editors,
|
|
87
|
+
message: `"${collection}" names "${declared.editors}" as its editors, which must be one of its own string[] fields holding user ids.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (declared.anonymous) {
|
|
91
|
+
problems.push({ code: 'data_editors_anonymous', collection, message: `"${collection}" can't be anonymous and name its editors.` });
|
|
92
|
+
}
|
|
93
|
+
return problems;
|
|
94
|
+
}
|
|
95
|
+
/** The plain list a record is kept with: its editors, or null for anyone who may write the instance. */
|
|
96
|
+
export const editorsOf = readersOf;
|
package/src/schema.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { FieldTypeInvalid, type FieldType } from './field-types.js';
|
|
|
3
3
|
import { type OpenProblemCode } from './open-schema.js';
|
|
4
4
|
import { type ConfirmProblemCode } from './confirm-email.js';
|
|
5
5
|
import { type AnonymousProblemCode, type AnonymousSpec } from './anonymous.js';
|
|
6
|
+
import { type EditorsProblemCode, type ReadersProblemCode } from './readers.js';
|
|
6
7
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
|
|
7
8
|
/**
|
|
8
9
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -163,9 +164,11 @@ declare const extensionShape: {
|
|
|
163
164
|
group: z.ZodString;
|
|
164
165
|
minimum: z.ZodOptional<z.ZodNumber>;
|
|
165
166
|
}, z.core.$strict>>;
|
|
167
|
+
readers: z.ZodOptional<z.ZodString>;
|
|
168
|
+
editors: z.ZodOptional<z.ZodString>;
|
|
166
169
|
}, z.core.$strip>>>;
|
|
167
170
|
tools: z.ZodOptional<z.ZodObject<{
|
|
168
|
-
generated: z.ZodOptional<z.ZodBoolean
|
|
171
|
+
generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
|
|
169
172
|
custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
170
173
|
name: z.ZodString;
|
|
171
174
|
description: z.ZodString;
|
|
@@ -241,7 +244,7 @@ export interface DataProblem {
|
|
|
241
244
|
field?: string;
|
|
242
245
|
message: string;
|
|
243
246
|
}
|
|
244
|
-
export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_key_taken' | 'placement_screen_unknown' | 'placement_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_chat_card_shape' | 'grant_tool_cross_app_self' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
247
|
+
export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | ReadersProblemCode | EditorsProblemCode | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_key_taken' | 'placement_screen_unknown' | 'placement_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_chat_card_shape' | 'grant_tool_cross_app_self' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
245
248
|
type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
|
|
246
249
|
/**
|
|
247
250
|
* Everything wrong with an app's additions that a type cannot say.
|
|
@@ -340,9 +343,11 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
340
343
|
group: z.ZodString;
|
|
341
344
|
minimum: z.ZodOptional<z.ZodNumber>;
|
|
342
345
|
}, z.core.$strict>>;
|
|
346
|
+
readers: z.ZodOptional<z.ZodString>;
|
|
347
|
+
editors: z.ZodOptional<z.ZodString>;
|
|
343
348
|
}, z.core.$strip>>>;
|
|
344
349
|
tools: z.ZodOptional<z.ZodObject<{
|
|
345
|
-
generated: z.ZodOptional<z.ZodBoolean
|
|
350
|
+
generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
|
|
346
351
|
custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
347
352
|
name: z.ZodString;
|
|
348
353
|
description: z.ZodString;
|
|
@@ -476,9 +481,11 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
476
481
|
group: z.ZodString;
|
|
477
482
|
minimum: z.ZodOptional<z.ZodNumber>;
|
|
478
483
|
}, z.core.$strict>>;
|
|
484
|
+
readers: z.ZodOptional<z.ZodString>;
|
|
485
|
+
editors: z.ZodOptional<z.ZodString>;
|
|
479
486
|
}, z.core.$strip>>>;
|
|
480
487
|
tools: z.ZodOptional<z.ZodObject<{
|
|
481
|
-
generated: z.ZodOptional<z.ZodBoolean
|
|
488
|
+
generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
|
|
482
489
|
custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
483
490
|
name: z.ZodString;
|
|
484
491
|
description: z.ZodString;
|
|
@@ -647,9 +654,11 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
647
654
|
group: z.ZodString;
|
|
648
655
|
minimum: z.ZodOptional<z.ZodNumber>;
|
|
649
656
|
}, z.core.$strict>>;
|
|
657
|
+
readers: z.ZodOptional<z.ZodString>;
|
|
658
|
+
editors: z.ZodOptional<z.ZodString>;
|
|
650
659
|
}, z.core.$strip>>>;
|
|
651
660
|
tools: z.ZodOptional<z.ZodObject<{
|
|
652
|
-
generated: z.ZodOptional<z.ZodBoolean
|
|
661
|
+
generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
|
|
653
662
|
custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
654
663
|
name: z.ZodString;
|
|
655
664
|
description: z.ZodString;
|
|
@@ -822,9 +831,11 @@ export declare const newAppManifestSchema: z.ZodObject<{
|
|
|
822
831
|
group: z.ZodString;
|
|
823
832
|
minimum: z.ZodOptional<z.ZodNumber>;
|
|
824
833
|
}, z.core.$strict>>;
|
|
834
|
+
readers: z.ZodOptional<z.ZodString>;
|
|
835
|
+
editors: z.ZodOptional<z.ZodString>;
|
|
825
836
|
}, z.core.$strip>>>;
|
|
826
837
|
tools: z.ZodOptional<z.ZodObject<{
|
|
827
|
-
generated: z.ZodOptional<z.ZodBoolean
|
|
838
|
+
generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
|
|
828
839
|
custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
829
840
|
name: z.ZodString;
|
|
830
841
|
description: z.ZodString;
|
|
@@ -914,6 +925,10 @@ export interface CollectionSpec {
|
|
|
914
925
|
definesFieldsOf?: string;
|
|
915
926
|
/** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
|
|
916
927
|
anonymous?: AnonymousSpec;
|
|
928
|
+
/** The string[] field naming who alone may read a record, when it names anyone (P16). */
|
|
929
|
+
readers?: string;
|
|
930
|
+
/** The string[] field naming who alone may change a record, when it names anyone (DW06). */
|
|
931
|
+
editors?: string;
|
|
917
932
|
}
|
|
918
933
|
/**
|
|
919
934
|
* The singular a collection's tools are named with: the manifest's, else the
|
package/src/schema.js
CHANGED
|
@@ -5,6 +5,7 @@ import { migrationsSchema } from "./migrations.js";
|
|
|
5
5
|
import { openSchemaProblems } from "./open-schema.js";
|
|
6
6
|
import { confirmEmailProblems, confirmEmailSchema } from "./confirm-email.js";
|
|
7
7
|
import { anonymousProblems, anonymousSchema, anonymousSpec } from "./anonymous.js";
|
|
8
|
+
import { editorsProblems, editorsSchema, READERS_LIMIT, readersProblems, readersSchema } from "./readers.js";
|
|
8
9
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
|
|
9
10
|
/**
|
|
10
11
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -152,6 +153,10 @@ const collectionSchema = z.object({
|
|
|
152
153
|
* batch, a watch and a create naming its own writer.
|
|
153
154
|
*/
|
|
154
155
|
anonymous: anonymousSchema.optional(),
|
|
156
|
+
/** One of its string[] fields: the user ids who alone may read a record, when it names any (P16). */
|
|
157
|
+
readers: readersSchema.optional(),
|
|
158
|
+
/** One of its string[] fields: the user ids who alone may change a record, when it names any (DW06). */
|
|
159
|
+
editors: editorsSchema.optional(),
|
|
155
160
|
});
|
|
156
161
|
const screenSchema = z.object({
|
|
157
162
|
/**
|
|
@@ -197,7 +202,12 @@ const customToolSchema = z.object({
|
|
|
197
202
|
});
|
|
198
203
|
const toolsSchema = z.object({
|
|
199
204
|
/** Off only when the app supplies every tool itself (A3-F08). */
|
|
200
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Off only when the app supplies every tool itself. `"read"` keeps the
|
|
207
|
+
* generated reads (get, list, search) and leaves every write to the app's
|
|
208
|
+
* own tools, where its rules about who may change what live (DW06).
|
|
209
|
+
*/
|
|
210
|
+
generated: z.union([z.boolean(), z.literal('read')]).optional(),
|
|
201
211
|
custom: z.array(customToolSchema).max(MAX_CUSTOM_TOOLS).optional(),
|
|
202
212
|
});
|
|
203
213
|
/** Most secrets one app may declare (ADR-A24). */
|
|
@@ -389,6 +399,11 @@ export function dataProblems(additions, options = {}) {
|
|
|
389
399
|
for (const [collection, declared] of collections) {
|
|
390
400
|
problems.push(...anonymousProblems(collection, declared));
|
|
391
401
|
}
|
|
402
|
+
// A collection whose records name their readers names a list of user ids to keep them in (P16).
|
|
403
|
+
for (const [collection, declared] of collections) {
|
|
404
|
+
problems.push(...readersProblems(collection, declared));
|
|
405
|
+
problems.push(...editorsProblems(collection, declared));
|
|
406
|
+
}
|
|
392
407
|
// What the app keeps must be what it asks to keep (A3-F06-S01): a
|
|
393
408
|
// collection the grants leave out would be data a workspace never agreed to
|
|
394
409
|
// hold, found only when the first write is refused.
|
|
@@ -490,7 +505,7 @@ export function dataProblems(additions, options = {}) {
|
|
|
490
505
|
function folderProblems(additions) {
|
|
491
506
|
const problems = [];
|
|
492
507
|
const custom = new Map((additions.tools?.custom ?? []).map(tool => [tool.name, tool]));
|
|
493
|
-
const generatedWrites = additions.tools?.generated === false
|
|
508
|
+
const generatedWrites = additions.tools?.generated === false || additions.tools?.generated === 'read'
|
|
494
509
|
? new Set()
|
|
495
510
|
: new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
|
|
496
511
|
const label = labelOf(name, declared.label);
|
|
@@ -551,15 +566,11 @@ function customToolProblems(additions, options) {
|
|
|
551
566
|
: new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
|
|
552
567
|
const label = labelOf(name, declared.label);
|
|
553
568
|
const plural = `${label}s`;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
`delete_${label}`,
|
|
559
|
-
`list_${plural}`,
|
|
560
|
-
`search_${plural}`,
|
|
561
|
-
`batch_${plural}`,
|
|
562
|
-
];
|
|
569
|
+
const reads = [`get_${label}`, `list_${plural}`, `search_${plural}`];
|
|
570
|
+
// With only reads generated, the write names are the app's to use.
|
|
571
|
+
return additions.tools?.generated === 'read'
|
|
572
|
+
? reads
|
|
573
|
+
: [...reads, `create_${label}`, `update_${label}`, `delete_${label}`, `batch_${plural}`];
|
|
563
574
|
}));
|
|
564
575
|
const seen = new Set();
|
|
565
576
|
const granted = additions.grants?.tools ?? [];
|
|
@@ -720,7 +731,13 @@ export function collectionsOf(manifest) {
|
|
|
720
731
|
const tableField = declared.openSchema?.table ?? owner?.flag.table;
|
|
721
732
|
const fields = Object.fromEntries(Object.entries(declared.schema).map(([field, raw]) => {
|
|
722
733
|
const type = parseFieldType(raw);
|
|
723
|
-
|
|
734
|
+
if (field === tableField)
|
|
735
|
+
return [field, { ...type, plain: true }];
|
|
736
|
+
// A record's readers or editors may name a whole team (P16, DW06).
|
|
737
|
+
return [
|
|
738
|
+
field,
|
|
739
|
+
(field === declared.readers || field === declared.editors) && type.kind === 'string[]' ? { ...type, maxEntries: READERS_LIMIT } : type,
|
|
740
|
+
];
|
|
724
741
|
}));
|
|
725
742
|
const label = labelOf(name, declared.label);
|
|
726
743
|
const names = Object.keys(fields);
|
|
@@ -738,6 +755,8 @@ export function collectionsOf(manifest) {
|
|
|
738
755
|
: {}),
|
|
739
756
|
...(owner ? { definesFieldsOf: owner.name } : {}),
|
|
740
757
|
...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
|
|
758
|
+
...(declared.readers ? { readers: declared.readers } : {}),
|
|
759
|
+
...(declared.editors ? { editors: declared.editors } : {}),
|
|
741
760
|
};
|
|
742
761
|
});
|
|
743
762
|
}
|