@brydio/manifest 0.1.0-alpha.34 → 0.1.0-alpha.36
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/anonymous.d.ts +42 -0
- package/src/anonymous.js +86 -0
- package/src/confirm-email.d.ts +40 -0
- package/src/confirm-email.js +78 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/migrations.d.ts +1 -1
- package/src/migrations.js +29 -0
- package/src/schema.d.ts +55 -1
- package/src/schema.js +27 -0
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.36",
|
|
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": "2d8e769d02c74fe201ce734f6d77def59ea99d53",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"types": "./src/index.d.ts",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* An anonymous collection (P13): answers nobody can tie back to the person
|
|
4
|
+
* who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
|
|
5
|
+
* the two change together.
|
|
6
|
+
*
|
|
7
|
+
* An app only declares it: which structured field groups the answers (a
|
|
8
|
+
* form, a survey round: a number, a choice, a date), and how many answers a
|
|
9
|
+
* group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
|
|
10
|
+
* on the record, times cut to the day, no live change told, no update, a
|
|
11
|
+
* create naming its own writer refused, and reads only of a whole group of
|
|
12
|
+
* at least `minimum` answers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ANONYMOUS_LIMITS: {
|
|
15
|
+
/** The fewest answers a group, or a narrowed read of one, may be read at. */
|
|
16
|
+
readonly minimum: 5;
|
|
17
|
+
/** The most an app may ask for. */
|
|
18
|
+
readonly maximum: 1000;
|
|
19
|
+
};
|
|
20
|
+
/** What the store writes as `created_by` on every anonymous record. */
|
|
21
|
+
export declare const ANONYMOUS_WRITER = "anonymous";
|
|
22
|
+
export declare const anonymousSchema: z.ZodObject<{
|
|
23
|
+
group: z.ZodString;
|
|
24
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type AnonymousDeclared = z.infer<typeof anonymousSchema>;
|
|
27
|
+
/** As the host reads it, on `CollectionSpec.anonymous`. */
|
|
28
|
+
export interface AnonymousSpec {
|
|
29
|
+
group: string;
|
|
30
|
+
minimum: number;
|
|
31
|
+
}
|
|
32
|
+
export type AnonymousProblemCode = 'data_anonymous_group' | 'data_anonymous_member' | 'data_anonymous_minimum' | 'data_anonymous_coedit';
|
|
33
|
+
export declare function anonymousProblems(collection: string, declared: {
|
|
34
|
+
schema: Record<string, unknown>;
|
|
35
|
+
anonymous?: AnonymousDeclared;
|
|
36
|
+
}): {
|
|
37
|
+
code: AnonymousProblemCode;
|
|
38
|
+
collection: string;
|
|
39
|
+
field?: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}[];
|
|
42
|
+
export declare function anonymousSpec(declared: AnonymousDeclared): AnonymousSpec;
|
package/src/anonymous.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FIELD_LIMITS, isStructured, parseFieldType, } from "./field-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* An anonymous collection (P13): answers nobody can tie back to the person
|
|
5
|
+
* who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
|
|
6
|
+
* the two change together.
|
|
7
|
+
*
|
|
8
|
+
* An app only declares it: which structured field groups the answers (a
|
|
9
|
+
* form, a survey round: a number, a choice, a date), and how many answers a
|
|
10
|
+
* group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
|
|
11
|
+
* on the record, times cut to the day, no live change told, no update, a
|
|
12
|
+
* create naming its own writer refused, and reads only of a whole group of
|
|
13
|
+
* at least `minimum` answers.
|
|
14
|
+
*/
|
|
15
|
+
export const ANONYMOUS_LIMITS = {
|
|
16
|
+
/** The fewest answers a group, or a narrowed read of one, may be read at. */
|
|
17
|
+
minimum: 5,
|
|
18
|
+
/** The most an app may ask for. */
|
|
19
|
+
maximum: 1000,
|
|
20
|
+
};
|
|
21
|
+
/** What the store writes as `created_by` on every anonymous record. */
|
|
22
|
+
export const ANONYMOUS_WRITER = 'anonymous';
|
|
23
|
+
export const anonymousSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
/** A structured field of the same collection, never a member: which group an answer belongs to. */
|
|
26
|
+
group: z.string().min(1).max(FIELD_LIMITS.nameChars),
|
|
27
|
+
/** At least 5, the default. */
|
|
28
|
+
minimum: z.number().int().max(ANONYMOUS_LIMITS.maximum).optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
export function anonymousProblems(collection, declared) {
|
|
32
|
+
const anonymous = declared.anonymous;
|
|
33
|
+
if (!anonymous)
|
|
34
|
+
return [];
|
|
35
|
+
const problems = [];
|
|
36
|
+
const types = new Map();
|
|
37
|
+
for (const [field, raw] of Object.entries(declared.schema)) {
|
|
38
|
+
try {
|
|
39
|
+
types.set(field, parseFieldType(raw));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Refused as a field type already.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Kept in plain, so a read can name its group; words never are.
|
|
46
|
+
const group = types.get(anonymous.group);
|
|
47
|
+
if (!group || !isStructured(group) || group.kind === 'member') {
|
|
48
|
+
problems.push({
|
|
49
|
+
code: 'data_anonymous_group',
|
|
50
|
+
collection,
|
|
51
|
+
field: anonymous.group,
|
|
52
|
+
message: `${collection}.anonymous.group must name one of its structured fields (a number, choice, date, boolean, token or project); ${anonymous.group} is not.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (anonymous.minimum !== undefined && anonymous.minimum < ANONYMOUS_LIMITS.minimum) {
|
|
56
|
+
problems.push({
|
|
57
|
+
code: 'data_anonymous_minimum',
|
|
58
|
+
collection,
|
|
59
|
+
message: `${collection}.anonymous.minimum is at least ${ANONYMOUS_LIMITS.minimum}: fewer answers than that can be told apart.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const [field, type] of types) {
|
|
63
|
+
// A member field names a person on the record, which is what this is for not doing.
|
|
64
|
+
if (type.kind === 'member') {
|
|
65
|
+
problems.push({
|
|
66
|
+
code: 'data_anonymous_member',
|
|
67
|
+
collection,
|
|
68
|
+
field,
|
|
69
|
+
message: `${collection} is anonymous, so it can't name a member: ${field} would.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Writing together shows who is typing, and changes a record after it is given.
|
|
73
|
+
if (type.coedit) {
|
|
74
|
+
problems.push({
|
|
75
|
+
code: 'data_anonymous_coedit',
|
|
76
|
+
collection,
|
|
77
|
+
field,
|
|
78
|
+
message: `${collection} is anonymous, so its answers can't be written together: ${field} would be.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return problems;
|
|
83
|
+
}
|
|
84
|
+
export function anonymousSpec(declared) {
|
|
85
|
+
return { group: declared.group, minimum: declared.minimum ?? ANONYMOUS_LIMITS.minimum };
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The confirmation email of a public form (FO04). Mirrors Brydio's
|
|
4
|
+
* `apps/api/src/apps/manifest/confirm-email.ts`; the two change together.
|
|
5
|
+
*
|
|
6
|
+
* An app only declares it: which field of a `publicSubmit` collection holds
|
|
7
|
+
* the visitor's address, and a short plain text to say. Brydio decides the
|
|
8
|
+
* rest — when it is sent, to whom, how often, from whom — so an anonymous
|
|
9
|
+
* page can never be used to send anybody anything else.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CONFIRM_LIMITS: {
|
|
12
|
+
readonly subjectChars: 120;
|
|
13
|
+
readonly messageChars: 600;
|
|
14
|
+
};
|
|
15
|
+
export declare const confirmEmailSchema: z.ZodObject<{
|
|
16
|
+
field: z.ZodString;
|
|
17
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
18
|
+
message: z.ZodOptional<z.ZodString>;
|
|
19
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type ConfirmEmailDeclared = z.infer<typeof confirmEmailSchema>;
|
|
22
|
+
/** As the host reads it, on `CollectionSpec.confirmEmail`. */
|
|
23
|
+
export interface ConfirmEmailSpec {
|
|
24
|
+
field: string;
|
|
25
|
+
subject: string | null;
|
|
26
|
+
message: string | null;
|
|
27
|
+
link: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type ConfirmProblemCode = 'data_confirm_submit' | 'data_confirm_field' | 'data_confirm_text';
|
|
30
|
+
export declare function confirmEmailProblems(collection: string, declared: {
|
|
31
|
+
schema: Record<string, unknown>;
|
|
32
|
+
publicSubmit?: boolean;
|
|
33
|
+
confirmEmail?: ConfirmEmailDeclared;
|
|
34
|
+
}): {
|
|
35
|
+
code: ConfirmProblemCode;
|
|
36
|
+
collection: string;
|
|
37
|
+
field?: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}[];
|
|
40
|
+
export declare function confirmEmailSpec(declared: ConfirmEmailDeclared): ConfirmEmailSpec;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FIELD_LIMITS, parseFieldType } from "./field-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The confirmation email of a public form (FO04). Mirrors Brydio's
|
|
5
|
+
* `apps/api/src/apps/manifest/confirm-email.ts`; the two change together.
|
|
6
|
+
*
|
|
7
|
+
* An app only declares it: which field of a `publicSubmit` collection holds
|
|
8
|
+
* the visitor's address, and a short plain text to say. Brydio decides the
|
|
9
|
+
* rest — when it is sent, to whom, how often, from whom — so an anonymous
|
|
10
|
+
* page can never be used to send anybody anything else.
|
|
11
|
+
*/
|
|
12
|
+
export const CONFIRM_LIMITS = {
|
|
13
|
+
subjectChars: 120,
|
|
14
|
+
messageChars: 600,
|
|
15
|
+
};
|
|
16
|
+
export const confirmEmailSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
/** A `string` field of the same collection: the visitor's address. */
|
|
19
|
+
field: z.string().min(1).max(FIELD_LIMITS.nameChars),
|
|
20
|
+
subject: z.string().min(1).max(CONFIRM_LIMITS.subjectChars).optional(),
|
|
21
|
+
/** Plain text; a blank line starts a new paragraph. */
|
|
22
|
+
message: z.string().min(1).max(CONFIRM_LIMITS.messageChars).optional(),
|
|
23
|
+
/** One button back to the public page the visitor answered. */
|
|
24
|
+
link: z.boolean().optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
/** A link, an address a client would turn into one, or markup. */
|
|
28
|
+
const NOT_PLAIN = /(?:[a-z][a-z0-9+.-]*:\/\/|www\.|[<>]|\]\(|mailto:)/i;
|
|
29
|
+
export function confirmEmailProblems(collection, declared) {
|
|
30
|
+
const confirm = declared.confirmEmail;
|
|
31
|
+
if (!confirm)
|
|
32
|
+
return [];
|
|
33
|
+
const problems = [];
|
|
34
|
+
if (!declared.publicSubmit) {
|
|
35
|
+
problems.push({
|
|
36
|
+
code: 'data_confirm_submit',
|
|
37
|
+
collection,
|
|
38
|
+
message: `${collection} sends a confirmation email but visitors can't submit to it: mark it publicSubmit.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const raw = declared.schema[confirm.field];
|
|
42
|
+
let kind = null;
|
|
43
|
+
try {
|
|
44
|
+
kind = raw === undefined ? null : parseFieldType(raw).kind;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
kind = null;
|
|
48
|
+
}
|
|
49
|
+
if (kind !== 'string') {
|
|
50
|
+
problems.push({
|
|
51
|
+
code: 'data_confirm_field',
|
|
52
|
+
collection,
|
|
53
|
+
field: confirm.field,
|
|
54
|
+
message: `${collection}.confirmEmail.field must name one of its string fields; ${confirm.field} is not.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const [key, text] of [
|
|
58
|
+
['subject', confirm.subject],
|
|
59
|
+
['message', confirm.message],
|
|
60
|
+
]) {
|
|
61
|
+
if (text !== undefined && NOT_PLAIN.test(text)) {
|
|
62
|
+
problems.push({
|
|
63
|
+
code: 'data_confirm_text',
|
|
64
|
+
collection,
|
|
65
|
+
message: `${collection}.confirmEmail.${key} is plain text: no links, addresses or markup.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return problems;
|
|
70
|
+
}
|
|
71
|
+
export function confirmEmailSpec(declared) {
|
|
72
|
+
return {
|
|
73
|
+
field: declared.field,
|
|
74
|
+
subject: declared.subject?.trim() || null,
|
|
75
|
+
message: declared.message?.trim() || null,
|
|
76
|
+
link: declared.link === true,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes,
|
|
|
9
9
|
export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, type DocumentOf, type FieldKind, type FieldType, type FieldValue, type FieldsOf, } from './field-types.js';
|
|
10
10
|
export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, type AppManifestWithData, type CollectionSpec, type DataProblem, type DataProblemCode, type DataProblemOptions, type HomeCardSize, type ManifestExtensions, type SecretSpec, } from './schema.js';
|
|
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
|
+
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, type ConfirmEmailDeclared, type ConfirmEmailSpec, type ConfirmProblemCode, } from './confirm-email.js';
|
|
13
|
+
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, type AnonymousDeclared, type AnonymousProblemCode, type AnonymousSpec, } from './anonymous.js';
|
|
12
14
|
export { DOCUMENT_LIMITS } from './document-limits.js';
|
|
13
15
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
|
|
14
16
|
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
|
@@ -9,6 +9,8 @@ export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes,
|
|
|
9
9
|
export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, } from "./field-types.js";
|
|
10
10
|
export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, } from "./schema.js";
|
|
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
|
+
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, } from "./confirm-email.js";
|
|
13
|
+
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, } from "./anonymous.js";
|
|
12
14
|
export { DOCUMENT_LIMITS } from "./document-limits.js";
|
|
13
15
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
|
|
14
16
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
|
package/src/migrations.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ export type SchemaChange = {
|
|
|
115
115
|
* Hodler's `diffManifests` to show, and none of them moves a record.
|
|
116
116
|
*/
|
|
117
117
|
export declare function diffSchemas(from: unknown, to: unknown): SchemaChange[];
|
|
118
|
-
export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed';
|
|
118
|
+
export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed' | 'migration_anonymous_changed';
|
|
119
119
|
export interface MigrationProblem {
|
|
120
120
|
code: MigrationCode;
|
|
121
121
|
collection: string;
|
package/src/migrations.js
CHANGED
|
@@ -181,6 +181,35 @@ export function migrationProblems(from, to, steps) {
|
|
|
181
181
|
break;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
+
problems.push(...anonymityChanges(from, to));
|
|
185
|
+
return problems;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A kept collection that becomes anonymous, stops being, or moves its group
|
|
189
|
+
* (P13): no step can say what its records become. Records kept before kept
|
|
190
|
+
* their writers and times, and records kept since were read only by group.
|
|
191
|
+
*/
|
|
192
|
+
function anonymityChanges(from, to) {
|
|
193
|
+
const before = record(record(from).data);
|
|
194
|
+
const after = record(record(to).data);
|
|
195
|
+
const problems = [];
|
|
196
|
+
for (const [name, declared] of Object.entries(after)) {
|
|
197
|
+
if (!(name in before))
|
|
198
|
+
continue;
|
|
199
|
+
const was = record(record(before[name]).anonymous).group;
|
|
200
|
+
const now = record(record(declared).anonymous).group;
|
|
201
|
+
if (was === now)
|
|
202
|
+
continue;
|
|
203
|
+
problems.push({
|
|
204
|
+
code: 'migration_anonymous_changed',
|
|
205
|
+
collection: name,
|
|
206
|
+
message: was === undefined
|
|
207
|
+
? `${name} already keeps records with their writers, so it can't become anonymous; keep anonymous answers in a new collection.`
|
|
208
|
+
: now === undefined
|
|
209
|
+
? `${name} keeps anonymous answers, so it stays anonymous.`
|
|
210
|
+
: `${name}'s anonymous answers stay grouped by ${String(was)}.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
184
213
|
return problems;
|
|
185
214
|
}
|
|
186
215
|
/** The old schema with the steps applied, and every step that could not apply or changed nothing. */
|
package/src/schema.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { FieldTypeInvalid, type FieldType } from './field-types.js';
|
|
3
3
|
import { type OpenProblemCode } from './open-schema.js';
|
|
4
|
+
import { type ConfirmProblemCode } from './confirm-email.js';
|
|
5
|
+
import { type AnonymousProblemCode, type AnonymousSpec } from './anonymous.js';
|
|
4
6
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
|
|
5
7
|
/**
|
|
6
8
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -150,6 +152,16 @@ declare const extensionShape: {
|
|
|
150
152
|
}, z.core.$strict>>;
|
|
151
153
|
publicRead: z.ZodOptional<z.ZodBoolean>;
|
|
152
154
|
publicSubmit: z.ZodOptional<z.ZodBoolean>;
|
|
155
|
+
confirmEmail: z.ZodOptional<z.ZodObject<{
|
|
156
|
+
field: z.ZodString;
|
|
157
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
158
|
+
message: z.ZodOptional<z.ZodString>;
|
|
159
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
160
|
+
}, z.core.$strict>>;
|
|
161
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
162
|
+
group: z.ZodString;
|
|
163
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
164
|
+
}, z.core.$strict>>;
|
|
153
165
|
}, z.core.$strip>>>;
|
|
154
166
|
tools: z.ZodOptional<z.ZodObject<{
|
|
155
167
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -228,7 +240,7 @@ export interface DataProblem {
|
|
|
228
240
|
field?: string;
|
|
229
241
|
message: string;
|
|
230
242
|
}
|
|
231
|
-
export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | '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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
243
|
+
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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
232
244
|
type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
|
|
233
245
|
/**
|
|
234
246
|
* Everything wrong with an app's additions that a type cannot say.
|
|
@@ -305,6 +317,16 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
305
317
|
}, z.core.$strict>>;
|
|
306
318
|
publicRead: z.ZodOptional<z.ZodBoolean>;
|
|
307
319
|
publicSubmit: z.ZodOptional<z.ZodBoolean>;
|
|
320
|
+
confirmEmail: z.ZodOptional<z.ZodObject<{
|
|
321
|
+
field: z.ZodString;
|
|
322
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
323
|
+
message: z.ZodOptional<z.ZodString>;
|
|
324
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
325
|
+
}, z.core.$strict>>;
|
|
326
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
327
|
+
group: z.ZodString;
|
|
328
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
329
|
+
}, z.core.$strict>>;
|
|
308
330
|
}, z.core.$strip>>>;
|
|
309
331
|
tools: z.ZodOptional<z.ZodObject<{
|
|
310
332
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -430,6 +452,16 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
430
452
|
}, z.core.$strict>>;
|
|
431
453
|
publicRead: z.ZodOptional<z.ZodBoolean>;
|
|
432
454
|
publicSubmit: z.ZodOptional<z.ZodBoolean>;
|
|
455
|
+
confirmEmail: z.ZodOptional<z.ZodObject<{
|
|
456
|
+
field: z.ZodString;
|
|
457
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
458
|
+
message: z.ZodOptional<z.ZodString>;
|
|
459
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
460
|
+
}, z.core.$strict>>;
|
|
461
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
462
|
+
group: z.ZodString;
|
|
463
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
464
|
+
}, z.core.$strict>>;
|
|
433
465
|
}, z.core.$strip>>>;
|
|
434
466
|
tools: z.ZodOptional<z.ZodObject<{
|
|
435
467
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -590,6 +622,16 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
590
622
|
}, z.core.$strict>>;
|
|
591
623
|
publicRead: z.ZodOptional<z.ZodBoolean>;
|
|
592
624
|
publicSubmit: z.ZodOptional<z.ZodBoolean>;
|
|
625
|
+
confirmEmail: z.ZodOptional<z.ZodObject<{
|
|
626
|
+
field: z.ZodString;
|
|
627
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
628
|
+
message: z.ZodOptional<z.ZodString>;
|
|
629
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
630
|
+
}, z.core.$strict>>;
|
|
631
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
632
|
+
group: z.ZodString;
|
|
633
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
634
|
+
}, z.core.$strict>>;
|
|
593
635
|
}, z.core.$strip>>>;
|
|
594
636
|
tools: z.ZodOptional<z.ZodObject<{
|
|
595
637
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -754,6 +796,16 @@ export declare const newAppManifestSchema: z.ZodObject<{
|
|
|
754
796
|
}, z.core.$strict>>;
|
|
755
797
|
publicRead: z.ZodOptional<z.ZodBoolean>;
|
|
756
798
|
publicSubmit: z.ZodOptional<z.ZodBoolean>;
|
|
799
|
+
confirmEmail: z.ZodOptional<z.ZodObject<{
|
|
800
|
+
field: z.ZodString;
|
|
801
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
802
|
+
message: z.ZodOptional<z.ZodString>;
|
|
803
|
+
link: z.ZodOptional<z.ZodBoolean>;
|
|
804
|
+
}, z.core.$strict>>;
|
|
805
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
806
|
+
group: z.ZodString;
|
|
807
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
808
|
+
}, z.core.$strict>>;
|
|
757
809
|
}, z.core.$strip>>>;
|
|
758
810
|
tools: z.ZodOptional<z.ZodObject<{
|
|
759
811
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -844,6 +896,8 @@ export interface CollectionSpec {
|
|
|
844
896
|
};
|
|
845
897
|
/** On a companion: the open collection whose fields its records define (P5). */
|
|
846
898
|
definesFieldsOf?: string;
|
|
899
|
+
/** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
|
|
900
|
+
anonymous?: AnonymousSpec;
|
|
847
901
|
}
|
|
848
902
|
/**
|
|
849
903
|
* The singular a collection's tools are named with: the manifest's, else the
|
package/src/schema.js
CHANGED
|
@@ -3,6 +3,8 @@ import { MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema as manifestSchema }
|
|
|
3
3
|
import { COLLECTION_NAME, FIELD_LIMITS, FIELD_NAME, FieldTypeInvalid, isSearchable, isSortable, isStructured, parseFieldType, RESERVED_FIELDS, } from "./field-types.js";
|
|
4
4
|
import { migrationsSchema } from "./migrations.js";
|
|
5
5
|
import { openSchemaProblems } from "./open-schema.js";
|
|
6
|
+
import { confirmEmailProblems, confirmEmailSchema } from "./confirm-email.js";
|
|
7
|
+
import { anonymousProblems, anonymousSchema, anonymousSpec } from "./anonymous.js";
|
|
6
8
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
|
|
7
9
|
/**
|
|
8
10
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -130,6 +132,22 @@ const collectionSchema = z.object({
|
|
|
130
132
|
* `publicRead` is set too (P3).
|
|
131
133
|
*/
|
|
132
134
|
publicSubmit: z.boolean().optional(),
|
|
135
|
+
/**
|
|
136
|
+
* Brydio emails the visitor once after a public submission here (FO04):
|
|
137
|
+
* `field` names a `string` field holding their address; `subject` and
|
|
138
|
+
* `message` are short plain text; `link` adds a button back to the page.
|
|
139
|
+
* Needs `publicSubmit`. Brydio decides when, how often and from whom.
|
|
140
|
+
*/
|
|
141
|
+
confirmEmail: confirmEmailSchema.optional(),
|
|
142
|
+
/**
|
|
143
|
+
* Answers nobody can tie to who gave them (P13): `group` names a
|
|
144
|
+
* structured field (a number, choice, date, boolean, token or project)
|
|
145
|
+
* the answers are read by, a whole group at a time and only once it holds
|
|
146
|
+
* `minimum` answers (at least 5, the default). Brydio keeps no writer,
|
|
147
|
+
* cuts times to the day, tells no live change, and refuses an update, a
|
|
148
|
+
* batch, a watch and a create naming its own writer.
|
|
149
|
+
*/
|
|
150
|
+
anonymous: anonymousSchema.optional(),
|
|
133
151
|
});
|
|
134
152
|
const screenSchema = z.object({
|
|
135
153
|
/**
|
|
@@ -359,6 +377,14 @@ export function dataProblems(additions, options = {}) {
|
|
|
359
377
|
}
|
|
360
378
|
companions.set(declared.openSchema.fields, collection);
|
|
361
379
|
}
|
|
380
|
+
// A confirmation email names a string field of a collection visitors submit to (FO04).
|
|
381
|
+
for (const [collection, declared] of collections) {
|
|
382
|
+
problems.push(...confirmEmailProblems(collection, declared));
|
|
383
|
+
}
|
|
384
|
+
// An anonymous collection groups its answers by one of its structured fields, names nobody (P13).
|
|
385
|
+
for (const [collection, declared] of collections) {
|
|
386
|
+
problems.push(...anonymousProblems(collection, declared));
|
|
387
|
+
}
|
|
362
388
|
// What the app keeps must be what it asks to keep (A3-F06-S01): a
|
|
363
389
|
// collection the grants leave out would be data a workspace never agreed to
|
|
364
390
|
// hold, found only when the first write is refused.
|
|
@@ -669,6 +695,7 @@ export function collectionsOf(manifest) {
|
|
|
669
695
|
? { openSchema: { fields: declared.openSchema.fields, table: declared.openSchema.table ?? null } }
|
|
670
696
|
: {}),
|
|
671
697
|
...(owner ? { definesFieldsOf: owner.name } : {}),
|
|
698
|
+
...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
|
|
672
699
|
};
|
|
673
700
|
});
|
|
674
701
|
}
|