@brydio/manifest 0.1.0-alpha.38 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brydio/manifest",
3
- "version": "0.1.0-alpha.38",
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": "4b6c80dcb849195171699f16022aab1f55d6e3b6",
13
+ "gitHead": "4b20d567d9361683e104ec609d231111bd93bcbf",
14
14
  "exports": {
15
15
  ".": {
16
16
  "types": "./src/index.d.ts",
@@ -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).
@@ -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,7 +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 { READERS_LIMIT, readersOf, readersProblems, type ReadersProblemCode } from './readers.js';
14
+ export { editorsOf, editorsProblems, READERS_LIMIT, readersOf, readersProblems, type EditorsProblemCode, type ReadersProblemCode } from './readers.js';
15
15
  export { DOCUMENT_LIMITS } from './document-limits.js';
16
16
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
17
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,7 +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 { READERS_LIMIT, readersOf, readersProblems } from "./readers.js";
14
+ export { editorsOf, editorsProblems, READERS_LIMIT, readersOf, readersProblems } from "./readers.js";
15
15
  export { DOCUMENT_LIMITS } from "./document-limits.js";
16
16
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
17
17
  export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
package/src/readers.d.ts CHANGED
@@ -29,3 +29,26 @@ export declare function readersProblems(collection: string, declared: {
29
29
  }[];
30
30
  /** The plain list a record is kept with: its readers, or null for everyone who can read the instance. */
31
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 CHANGED
@@ -57,3 +57,40 @@ export function readersOf(field, body) {
57
57
  const ids = [...new Set(value.filter((one) => typeof one === 'string' && one.length > 0))];
58
58
  return ids.length ? ids.sort() : null;
59
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,7 +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 ReadersProblemCode } from './readers.js';
6
+ import { type EditorsProblemCode, type ReadersProblemCode } from './readers.js';
7
7
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
8
8
  /**
9
9
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -165,9 +165,10 @@ declare const extensionShape: {
165
165
  minimum: z.ZodOptional<z.ZodNumber>;
166
166
  }, z.core.$strict>>;
167
167
  readers: z.ZodOptional<z.ZodString>;
168
+ editors: z.ZodOptional<z.ZodString>;
168
169
  }, z.core.$strip>>>;
169
170
  tools: z.ZodOptional<z.ZodObject<{
170
- generated: z.ZodOptional<z.ZodBoolean>;
171
+ generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
171
172
  custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
172
173
  name: z.ZodString;
173
174
  description: z.ZodString;
@@ -243,7 +244,7 @@ export interface DataProblem {
243
244
  field?: string;
244
245
  message: string;
245
246
  }
246
- export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | ReadersProblemCode | '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';
247
248
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
248
249
  /**
249
250
  * Everything wrong with an app's additions that a type cannot say.
@@ -343,9 +344,10 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
343
344
  minimum: z.ZodOptional<z.ZodNumber>;
344
345
  }, z.core.$strict>>;
345
346
  readers: z.ZodOptional<z.ZodString>;
347
+ editors: z.ZodOptional<z.ZodString>;
346
348
  }, z.core.$strip>>>;
347
349
  tools: z.ZodOptional<z.ZodObject<{
348
- generated: z.ZodOptional<z.ZodBoolean>;
350
+ generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
349
351
  custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
350
352
  name: z.ZodString;
351
353
  description: z.ZodString;
@@ -480,9 +482,10 @@ export declare const storedExtensionsSchema: z.ZodObject<{
480
482
  minimum: z.ZodOptional<z.ZodNumber>;
481
483
  }, z.core.$strict>>;
482
484
  readers: z.ZodOptional<z.ZodString>;
485
+ editors: z.ZodOptional<z.ZodString>;
483
486
  }, z.core.$strip>>>;
484
487
  tools: z.ZodOptional<z.ZodObject<{
485
- generated: z.ZodOptional<z.ZodBoolean>;
488
+ generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
486
489
  custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
487
490
  name: z.ZodString;
488
491
  description: z.ZodString;
@@ -652,9 +655,10 @@ export declare const appManifestSchema: z.ZodObject<{
652
655
  minimum: z.ZodOptional<z.ZodNumber>;
653
656
  }, z.core.$strict>>;
654
657
  readers: z.ZodOptional<z.ZodString>;
658
+ editors: z.ZodOptional<z.ZodString>;
655
659
  }, z.core.$strip>>>;
656
660
  tools: z.ZodOptional<z.ZodObject<{
657
- generated: z.ZodOptional<z.ZodBoolean>;
661
+ generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
658
662
  custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
659
663
  name: z.ZodString;
660
664
  description: z.ZodString;
@@ -828,9 +832,10 @@ export declare const newAppManifestSchema: z.ZodObject<{
828
832
  minimum: z.ZodOptional<z.ZodNumber>;
829
833
  }, z.core.$strict>>;
830
834
  readers: z.ZodOptional<z.ZodString>;
835
+ editors: z.ZodOptional<z.ZodString>;
831
836
  }, z.core.$strip>>>;
832
837
  tools: z.ZodOptional<z.ZodObject<{
833
- generated: z.ZodOptional<z.ZodBoolean>;
838
+ generated: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"read">]>>;
834
839
  custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
835
840
  name: z.ZodString;
836
841
  description: z.ZodString;
@@ -922,6 +927,8 @@ export interface CollectionSpec {
922
927
  anonymous?: AnonymousSpec;
923
928
  /** The string[] field naming who alone may read a record, when it names anyone (P16). */
924
929
  readers?: string;
930
+ /** The string[] field naming who alone may change a record, when it names anyone (DW06). */
931
+ editors?: string;
925
932
  }
926
933
  /**
927
934
  * The singular a collection's tools are named with: the manifest's, else the
package/src/schema.js CHANGED
@@ -5,7 +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 { readersProblems, readersSchema } from "./readers.js";
8
+ import { editorsProblems, editorsSchema, READERS_LIMIT, readersProblems, readersSchema } from "./readers.js";
9
9
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
10
10
  /**
11
11
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -155,6 +155,8 @@ const collectionSchema = z.object({
155
155
  anonymous: anonymousSchema.optional(),
156
156
  /** One of its string[] fields: the user ids who alone may read a record, when it names any (P16). */
157
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(),
158
160
  });
159
161
  const screenSchema = z.object({
160
162
  /**
@@ -200,7 +202,12 @@ const customToolSchema = z.object({
200
202
  });
201
203
  const toolsSchema = z.object({
202
204
  /** Off only when the app supplies every tool itself (A3-F08). */
203
- generated: z.boolean().optional(),
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(),
204
211
  custom: z.array(customToolSchema).max(MAX_CUSTOM_TOOLS).optional(),
205
212
  });
206
213
  /** Most secrets one app may declare (ADR-A24). */
@@ -395,6 +402,7 @@ export function dataProblems(additions, options = {}) {
395
402
  // A collection whose records name their readers names a list of user ids to keep them in (P16).
396
403
  for (const [collection, declared] of collections) {
397
404
  problems.push(...readersProblems(collection, declared));
405
+ problems.push(...editorsProblems(collection, declared));
398
406
  }
399
407
  // What the app keeps must be what it asks to keep (A3-F06-S01): a
400
408
  // collection the grants leave out would be data a workspace never agreed to
@@ -497,7 +505,7 @@ export function dataProblems(additions, options = {}) {
497
505
  function folderProblems(additions) {
498
506
  const problems = [];
499
507
  const custom = new Map((additions.tools?.custom ?? []).map(tool => [tool.name, tool]));
500
- const generatedWrites = additions.tools?.generated === false
508
+ const generatedWrites = additions.tools?.generated === false || additions.tools?.generated === 'read'
501
509
  ? new Set()
502
510
  : new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
503
511
  const label = labelOf(name, declared.label);
@@ -558,15 +566,11 @@ function customToolProblems(additions, options) {
558
566
  : new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
559
567
  const label = labelOf(name, declared.label);
560
568
  const plural = `${label}s`;
561
- return [
562
- `create_${label}`,
563
- `update_${label}`,
564
- `get_${label}`,
565
- `delete_${label}`,
566
- `list_${plural}`,
567
- `search_${plural}`,
568
- `batch_${plural}`,
569
- ];
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}`];
570
574
  }));
571
575
  const seen = new Set();
572
576
  const granted = additions.grants?.tools ?? [];
@@ -727,7 +731,13 @@ export function collectionsOf(manifest) {
727
731
  const tableField = declared.openSchema?.table ?? owner?.flag.table;
728
732
  const fields = Object.fromEntries(Object.entries(declared.schema).map(([field, raw]) => {
729
733
  const type = parseFieldType(raw);
730
- return [field, field === tableField ? { ...type, plain: true } : type];
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
+ ];
731
741
  }));
732
742
  const label = labelOf(name, declared.label);
733
743
  const names = Object.keys(fields);
@@ -746,6 +756,7 @@ export function collectionsOf(manifest) {
746
756
  ...(owner ? { definesFieldsOf: owner.name } : {}),
747
757
  ...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
748
758
  ...(declared.readers ? { readers: declared.readers } : {}),
759
+ ...(declared.editors ? { editors: declared.editors } : {}),
749
760
  };
750
761
  });
751
762
  }