@protoyard/blueprint 0.0.0-stage → 1.1.1
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/LICENSE +21 -0
- package/README.md +1145 -2
- package/dist/aggregate.d.ts +83 -0
- package/dist/aggregate.js +268 -0
- package/dist/app-model.d.ts +76 -0
- package/dist/app-model.js +287 -0
- package/dist/archetypes/index.d.ts +1 -0
- package/dist/archetypes/index.js +1 -0
- package/dist/archetypes/schema.d.ts +126 -0
- package/dist/archetypes/schema.js +77 -0
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +149 -0
- package/dist/authoring/deck-lint.d.ts +32 -0
- package/dist/authoring/deck-lint.js +567 -0
- package/dist/authoring/index.d.ts +1 -0
- package/dist/authoring/index.js +1 -0
- package/dist/authoring/select-deck-layouts.d.ts +50 -0
- package/dist/authoring/select-deck-layouts.js +213 -0
- package/dist/authoring-apis.d.ts +13 -0
- package/dist/authoring-apis.js +13 -0
- package/dist/authoring-validation.d.ts +65 -0
- package/dist/authoring-validation.js +267 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +30 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.js +294 -0
- package/dist/brief/archetypes.d.ts +28 -0
- package/dist/brief/archetypes.js +798 -0
- package/dist/brief/content-synthesize.d.ts +31 -0
- package/dist/brief/content-synthesize.js +210 -0
- package/dist/brief/deck-outlines.d.ts +96 -0
- package/dist/brief/deck-outlines.js +561 -0
- package/dist/brief/draft-bodies.d.ts +29 -0
- package/dist/brief/draft-bodies.js +224 -0
- package/dist/brief/fields.d.ts +19 -0
- package/dist/brief/fields.js +229 -0
- package/dist/brief/index.d.ts +8 -0
- package/dist/brief/index.js +8 -0
- package/dist/brief/kind-detection.d.ts +69 -0
- package/dist/brief/kind-detection.js +353 -0
- package/dist/brief/limits.d.ts +117 -0
- package/dist/brief/limits.js +217 -0
- package/dist/brief/page-doc-outlines.d.ts +80 -0
- package/dist/brief/page-doc-outlines.js +485 -0
- package/dist/brief/synthesize.d.ts +59 -0
- package/dist/brief/synthesize.js +511 -0
- package/dist/brief/text.d.ts +13 -0
- package/dist/brief/text.js +59 -0
- package/dist/canvas-screen.d.ts +31 -0
- package/dist/canvas-screen.js +33 -0
- package/dist/canvas-seed.d.ts +58 -0
- package/dist/canvas-seed.js +82 -0
- package/dist/computed/evaluate.d.ts +11 -0
- package/dist/computed/evaluate.js +101 -0
- package/dist/computed/index.d.ts +4 -0
- package/dist/computed/index.js +3 -0
- package/dist/computed/public.d.ts +14 -0
- package/dist/computed/public.js +19 -0
- package/dist/computed/schema.d.ts +10 -0
- package/dist/computed/schema.js +140 -0
- package/dist/computed/types.d.ts +70 -0
- package/dist/computed/types.js +1 -0
- package/dist/computed/validate.d.ts +6 -0
- package/dist/computed/validate.js +146 -0
- package/dist/csv-guard.d.ts +15 -0
- package/dist/csv-guard.js +29 -0
- package/dist/csv.d.ts +85 -0
- package/dist/csv.js +288 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +246 -0
- package/dist/form-schema.d.ts +124 -0
- package/dist/form-schema.js +300 -0
- package/dist/generator-version.d.ts +11 -0
- package/dist/generator-version.js +51 -0
- package/dist/generator.d.ts +34 -0
- package/dist/generator.js +445 -0
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +91 -0
- package/dist/import/csv-shape.d.ts +40 -0
- package/dist/import/csv-shape.js +265 -0
- package/dist/import/json-shape.d.ts +55 -0
- package/dist/import/json-shape.js +491 -0
- package/dist/import/limits.d.ts +13 -0
- package/dist/import/limits.js +14 -0
- package/dist/import/sqlite-schema.d.ts +36 -0
- package/dist/import/sqlite-schema.js +729 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +58 -0
- package/dist/kinds/anchors.d.ts +18 -0
- package/dist/kinds/anchors.js +46 -0
- package/dist/kinds/bodies.d.ts +6 -0
- package/dist/kinds/bodies.js +32 -0
- package/dist/kinds/brand-runtime.d.ts +19 -0
- package/dist/kinds/brand-runtime.js +35 -0
- package/dist/kinds/deck.d.ts +8 -0
- package/dist/kinds/deck.js +34 -0
- package/dist/kinds/doc.d.ts +19 -0
- package/dist/kinds/doc.js +41 -0
- package/dist/kinds/embed/index.d.ts +39 -0
- package/dist/kinds/embed/index.js +91 -0
- package/dist/kinds/envelope.d.ts +159 -0
- package/dist/kinds/envelope.js +78 -0
- package/dist/kinds/index.d.ts +11 -0
- package/dist/kinds/index.js +11 -0
- package/dist/kinds/kind.d.ts +36 -0
- package/dist/kinds/kind.js +55 -0
- package/dist/kinds/limits.d.ts +64 -0
- package/dist/kinds/limits.js +190 -0
- package/dist/kinds/page-doc-runtime.d.ts +37 -0
- package/dist/kinds/page-doc-runtime.js +55 -0
- package/dist/kinds/page.d.ts +16 -0
- package/dist/kinds/page.js +67 -0
- package/dist/kinds/parse.d.ts +18 -0
- package/dist/kinds/parse.js +27 -0
- package/dist/kinds/runtime.d.ts +128 -0
- package/dist/kinds/runtime.js +206 -0
- package/dist/kinds/services.d.ts +49 -0
- package/dist/kinds/services.js +249 -0
- package/dist/kinds/testing.d.ts +9 -0
- package/dist/kinds/testing.js +16 -0
- package/dist/migrations.d.ts +97 -0
- package/dist/migrations.js +351 -0
- package/dist/motion.d.ts +50 -0
- package/dist/motion.js +52 -0
- package/dist/notifications.d.ts +19 -0
- package/dist/notifications.js +177 -0
- package/dist/patch.d.ts +122 -0
- package/dist/patch.js +322 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +105 -0
- package/dist/runtime-definition.d.ts +39 -0
- package/dist/runtime-definition.js +74 -0
- package/dist/scenarios.d.ts +9 -0
- package/dist/scenarios.js +49 -0
- package/dist/schema/blueprint.schema.json +10191 -0
- package/dist/schema-json.d.ts +6 -0
- package/dist/schema-json.js +36 -0
- package/dist/schema.d.ts +6348 -0
- package/dist/schema.js +1269 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +96 -0
- package/dist/services.d.ts +89 -0
- package/dist/services.js +898 -0
- package/dist/share.d.ts +78 -0
- package/dist/share.js +234 -0
- package/dist/sheet-screen.d.ts +49 -0
- package/dist/sheet-screen.js +91 -0
- package/dist/storage.d.ts +43 -0
- package/dist/storage.js +179 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +173 -0
- package/dist/untrusted.d.ts +63 -0
- package/dist/untrusted.js +502 -0
- package/dist/validation-tasks.d.ts +178 -0
- package/dist/validation-tasks.js +199 -0
- package/dist/validation.d.ts +35 -0
- package/dist/validation.js +312 -0
- package/dist/variants.d.ts +112 -0
- package/dist/variants.js +283 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/schema.d.ts +50 -0
- package/dist/workflows/schema.js +34 -0
- package/dist/workflows/transition.d.ts +15 -0
- package/dist/workflows/transition.js +25 -0
- package/dist/workflows/validate.d.ts +17 -0
- package/dist/workflows/validate.js +140 -0
- package/package.json +120 -3
package/dist/share.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AnyBlueprint } from './kinds/envelope.ts';
|
|
2
|
+
import type { Blueprint } from './schema.ts';
|
|
3
|
+
import { type UntrustedBlueprintIssue } from './untrusted.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Share links (ADR-030): a blueprint travels in a URL fragment, `#bp=1.<data>`,
|
|
6
|
+
* where `<data>` is the canonical JSON, deflate-raw compressed, in base64url.
|
|
7
|
+
* Fragments never reach a server. Decoding treats the link as untrusted:
|
|
8
|
+
* size caps apply before any work, decompression stops at the loader's byte
|
|
9
|
+
* cap (no zip bombs), and the text goes through `loadUntrustedBlueprint`.
|
|
10
|
+
*
|
|
11
|
+
* Uses `CompressionStream` / `DecompressionStream` (browsers, Node 20+).
|
|
12
|
+
*/
|
|
13
|
+
/** Version prefix of the current share format. */
|
|
14
|
+
export declare const SHARE_LINK_VERSION = "1";
|
|
15
|
+
/** The fragment parameter a player reads: `#bp=<encoded>`. */
|
|
16
|
+
export declare const SHARE_FRAGMENT_PARAM = "bp";
|
|
17
|
+
/** Encoded links longer than this are refused (they stop fitting chat tools). */
|
|
18
|
+
export declare const MAX_SHARE_ENCODED_LENGTH: number;
|
|
19
|
+
/** Decompression stops past this many bytes: the untrusted loader's input cap. */
|
|
20
|
+
export declare const MAX_SHARE_DECODED_BYTES: number;
|
|
21
|
+
/** The same cap for links to any kind (`decodeDocumentShare`): the document loader's input cap. */
|
|
22
|
+
export declare const MAX_SHARE_DOCUMENT_DECODED_BYTES: number;
|
|
23
|
+
export type ShareLinkIssue = Omit<UntrustedBlueprintIssue, 'code'> & {
|
|
24
|
+
code: UntrustedBlueprintIssue['code'] | 'invalid-link' | 'unsupported-version' | 'unsupported-browser';
|
|
25
|
+
};
|
|
26
|
+
/** The browsers whose `CompressionStream` / `DecompressionStream` handle `deflate-raw`. */
|
|
27
|
+
export declare const SHARE_LINK_BROWSERS = "Chrome 103+, Safari 16.4+, Firefox 113+";
|
|
28
|
+
/** Whether this environment can encode and decode share links. */
|
|
29
|
+
export declare function supportsShareLinks(): boolean;
|
|
30
|
+
export type ShareDecodeResult = {
|
|
31
|
+
success: true;
|
|
32
|
+
blueprint: Blueprint;
|
|
33
|
+
issues: [];
|
|
34
|
+
} | {
|
|
35
|
+
success: false;
|
|
36
|
+
blueprint: null;
|
|
37
|
+
issues: ShareLinkIssue[];
|
|
38
|
+
};
|
|
39
|
+
export type DocumentShareDecodeResult = {
|
|
40
|
+
success: true;
|
|
41
|
+
blueprint: AnyBlueprint;
|
|
42
|
+
issues: [];
|
|
43
|
+
} | {
|
|
44
|
+
success: false;
|
|
45
|
+
blueprint: null;
|
|
46
|
+
issues: ShareLinkIssue[];
|
|
47
|
+
};
|
|
48
|
+
/** Thrown by `encodeBlueprintShare` when a blueprint does not fit in a link. */
|
|
49
|
+
export declare class ShareLinkTooLargeError extends Error {
|
|
50
|
+
readonly encodedLength: number;
|
|
51
|
+
constructor(encodedLength: number);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Encodes a blueprint of any kind as `1.<base64url(deflate-raw(canonical JSON))>`.
|
|
55
|
+
* The same blueprint always gives the same link, whatever its key order. Throws
|
|
56
|
+
* `ShareLinkTooLargeError` past `MAX_SHARE_ENCODED_LENGTH`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function encodeBlueprintShare(blueprint: AnyBlueprint): Promise<string>;
|
|
59
|
+
/** `#bp=<encoded>`, ready to append to the player's URL. */
|
|
60
|
+
export declare function blueprintShareFragment(encoded: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* The encoded blueprint in a URL fragment (`#bp=…`, alone or among other
|
|
63
|
+
* `&`-separated parameters), or null when there is none.
|
|
64
|
+
*/
|
|
65
|
+
export declare function readBlueprintShareFragment(hash: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Decodes an app share link payload (`1.<data>`, or a whole `#bp=` fragment)
|
|
68
|
+
* into a validated app blueprint or typed issues (`loadUntrustedBlueprint`).
|
|
69
|
+
* Never throws.
|
|
70
|
+
*/
|
|
71
|
+
export declare function decodeBlueprintShare(input: string): Promise<ShareDecodeResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Decodes a share link to a blueprint of any kind (app, deck, page, doc)
|
|
74
|
+
* through `loadUntrustedDocument`: content caps, multiline fields and the
|
|
75
|
+
* kind's body schema. Decompression stops at the document loader's byte cap.
|
|
76
|
+
* Never throws.
|
|
77
|
+
*/
|
|
78
|
+
export declare function decodeDocumentShare(input: string): Promise<DocumentShareDecodeResult>;
|
package/dist/share.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { canonicalJson } from "./identity.js";
|
|
2
|
+
import { DEFAULT_UNTRUSTED_DOCUMENT_LIMITS } from "./kinds/limits.js";
|
|
3
|
+
import { DEFAULT_UNTRUSTED_BLUEPRINT_LIMITS, loadUntrustedBlueprint, loadUntrustedDocument, } from "./untrusted.js";
|
|
4
|
+
/**
|
|
5
|
+
* Share links (ADR-030): a blueprint travels in a URL fragment, `#bp=1.<data>`,
|
|
6
|
+
* where `<data>` is the canonical JSON, deflate-raw compressed, in base64url.
|
|
7
|
+
* Fragments never reach a server. Decoding treats the link as untrusted:
|
|
8
|
+
* size caps apply before any work, decompression stops at the loader's byte
|
|
9
|
+
* cap (no zip bombs), and the text goes through `loadUntrustedBlueprint`.
|
|
10
|
+
*
|
|
11
|
+
* Uses `CompressionStream` / `DecompressionStream` (browsers, Node 20+).
|
|
12
|
+
*/
|
|
13
|
+
/** Version prefix of the current share format. */
|
|
14
|
+
export const SHARE_LINK_VERSION = '1';
|
|
15
|
+
/** The fragment parameter a player reads: `#bp=<encoded>`. */
|
|
16
|
+
export const SHARE_FRAGMENT_PARAM = 'bp';
|
|
17
|
+
/** Encoded links longer than this are refused (they stop fitting chat tools). */
|
|
18
|
+
export const MAX_SHARE_ENCODED_LENGTH = 16 * 1024;
|
|
19
|
+
/** Decompression stops past this many bytes: the untrusted loader's input cap. */
|
|
20
|
+
export const MAX_SHARE_DECODED_BYTES = DEFAULT_UNTRUSTED_BLUEPRINT_LIMITS.maxBytes;
|
|
21
|
+
/** The same cap for links to any kind (`decodeDocumentShare`): the document loader's input cap. */
|
|
22
|
+
export const MAX_SHARE_DOCUMENT_DECODED_BYTES = DEFAULT_UNTRUSTED_DOCUMENT_LIMITS.maxBytes;
|
|
23
|
+
const PREFIX = `${SHARE_LINK_VERSION}.`;
|
|
24
|
+
const BASE64URL = /^[A-Za-z0-9_-]*$/;
|
|
25
|
+
/** The browsers whose `CompressionStream` / `DecompressionStream` handle `deflate-raw`. */
|
|
26
|
+
export const SHARE_LINK_BROWSERS = 'Chrome 103+, Safari 16.4+, Firefox 113+';
|
|
27
|
+
/** Whether this environment can encode and decode share links. */
|
|
28
|
+
export function supportsShareLinks() {
|
|
29
|
+
if (typeof CompressionStream === 'undefined' || typeof DecompressionStream === 'undefined') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
new DecompressionStream('deflate-raw');
|
|
34
|
+
new CompressionStream('deflate-raw');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Thrown by `encodeBlueprintShare` when a blueprint does not fit in a link. */
|
|
42
|
+
export class ShareLinkTooLargeError extends Error {
|
|
43
|
+
encodedLength;
|
|
44
|
+
constructor(encodedLength) {
|
|
45
|
+
super(`This blueprint needs a ${encodedLength}-character link; links are limited to ${MAX_SHARE_ENCODED_LENGTH}. Share the blueprint.json file instead.`);
|
|
46
|
+
this.name = 'ShareLinkTooLargeError';
|
|
47
|
+
this.encodedLength = encodedLength;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fail = (code, message, fix) => ({
|
|
51
|
+
success: false,
|
|
52
|
+
blueprint: null,
|
|
53
|
+
issues: [{ code, path: [], message, fix }],
|
|
54
|
+
});
|
|
55
|
+
function streamOf(bytes) {
|
|
56
|
+
return new ReadableStream({
|
|
57
|
+
start(controller) {
|
|
58
|
+
controller.enqueue(bytes);
|
|
59
|
+
controller.close();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function collect(stream, cap) {
|
|
64
|
+
const reader = stream.getReader();
|
|
65
|
+
const chunks = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const { done, value } = await reader.read();
|
|
69
|
+
if (done)
|
|
70
|
+
break;
|
|
71
|
+
total += value.byteLength;
|
|
72
|
+
if (total > cap) {
|
|
73
|
+
await reader.cancel().catch(() => undefined);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
chunks.push(value);
|
|
77
|
+
}
|
|
78
|
+
const out = new Uint8Array(total);
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const chunk of chunks) {
|
|
81
|
+
out.set(chunk, offset);
|
|
82
|
+
offset += chunk.byteLength;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function toBase64Url(bytes) {
|
|
87
|
+
let binary = '';
|
|
88
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
89
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
90
|
+
}
|
|
91
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
92
|
+
}
|
|
93
|
+
function fromBase64Url(text) {
|
|
94
|
+
if (text.length % 4 === 1)
|
|
95
|
+
return null;
|
|
96
|
+
const padded = text.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (text.length % 4)) % 4);
|
|
97
|
+
try {
|
|
98
|
+
const binary = atob(padded);
|
|
99
|
+
const bytes = new Uint8Array(binary.length);
|
|
100
|
+
for (let i = 0; i < binary.length; i += 1)
|
|
101
|
+
bytes[i] = binary.charCodeAt(i);
|
|
102
|
+
return bytes;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encodes a blueprint of any kind as `1.<base64url(deflate-raw(canonical JSON))>`.
|
|
110
|
+
* The same blueprint always gives the same link, whatever its key order. Throws
|
|
111
|
+
* `ShareLinkTooLargeError` past `MAX_SHARE_ENCODED_LENGTH`.
|
|
112
|
+
*/
|
|
113
|
+
export async function encodeBlueprintShare(blueprint) {
|
|
114
|
+
if (!supportsShareLinks()) {
|
|
115
|
+
throw new Error(`This environment cannot create share links: it lacks CompressionStream("deflate-raw") (${SHARE_LINK_BROWSERS}). Share the blueprint.json file instead.`);
|
|
116
|
+
}
|
|
117
|
+
const { $schema: _schema, ...content } = blueprint;
|
|
118
|
+
const json = new TextEncoder().encode(canonicalJson(content));
|
|
119
|
+
const compressed = await collect(streamOf(json).pipeThrough(new CompressionStream('deflate-raw')), Number.POSITIVE_INFINITY);
|
|
120
|
+
const encoded = `${PREFIX}${toBase64Url(compressed ?? new Uint8Array())}`;
|
|
121
|
+
if (encoded.length > MAX_SHARE_ENCODED_LENGTH)
|
|
122
|
+
throw new ShareLinkTooLargeError(encoded.length);
|
|
123
|
+
return encoded;
|
|
124
|
+
}
|
|
125
|
+
/** `#bp=<encoded>`, ready to append to the player's URL. */
|
|
126
|
+
export function blueprintShareFragment(encoded) {
|
|
127
|
+
return `#${SHARE_FRAGMENT_PARAM}=${encoded}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The encoded blueprint in a URL fragment (`#bp=…`, alone or among other
|
|
131
|
+
* `&`-separated parameters), or null when there is none.
|
|
132
|
+
*/
|
|
133
|
+
export function readBlueprintShareFragment(hash) {
|
|
134
|
+
const body = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
135
|
+
for (const part of body.split('&')) {
|
|
136
|
+
const [key, ...rest] = part.split('=');
|
|
137
|
+
if (key === SHARE_FRAGMENT_PARAM)
|
|
138
|
+
return rest.join('=');
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/** The link payload's JSON text, or the issue that stopped it (size, version, encoding). */
|
|
143
|
+
async function inflateShare(input, maxBytes) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
146
|
+
const encoded = raw.startsWith('#') ? (readBlueprintShareFragment(raw) ?? '') : raw;
|
|
147
|
+
if (encoded.length > MAX_SHARE_ENCODED_LENGTH) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
result: fail('too-large', `The link is ${encoded.length} characters; share links are limited to ${MAX_SHARE_ENCODED_LENGTH}.`, 'Open the blueprint.json file instead of a link.'),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const dot = encoded.indexOf('.');
|
|
154
|
+
if (dot < 1) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
result: fail('invalid-link', 'The link has no format version.', 'Copy the whole link, including the part after "#bp=".'),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const version = encoded.slice(0, dot);
|
|
161
|
+
if (version !== SHARE_LINK_VERSION) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
result: fail('unsupported-version', `The link uses share format "${version.slice(0, 8)}"; this player reads format ${SHARE_LINK_VERSION}.`, 'Open the link in a newer player, or share the blueprint.json file.'),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (!supportsShareLinks()) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
result: fail('unsupported-browser', `This browser cannot open share links: it lacks DecompressionStream("deflate-raw") (${SHARE_LINK_BROWSERS}).`, 'Open the link in a current browser, or ask for the blueprint.json file and open it.'),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const data = encoded.slice(dot + 1);
|
|
174
|
+
const bytes = BASE64URL.test(data) && data.length > 0 ? fromBase64Url(data) : null;
|
|
175
|
+
if (!bytes) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
result: fail('invalid-link', 'The link is not valid base64url data.', 'Copy the whole link again; it may have been cut or changed.'),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
let inflated;
|
|
182
|
+
try {
|
|
183
|
+
inflated = await collect(streamOf(bytes).pipeThrough(new DecompressionStream('deflate-raw')), maxBytes);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
result: fail('invalid-link', 'The link data could not be decompressed.', 'Copy the whole link again; it may have been cut or changed.'),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (!inflated) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
result: fail('too-large', `The blueprint in this link is larger than ${maxBytes} bytes.`, 'Open the blueprint.json file instead of a link.'),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
let text;
|
|
198
|
+
try {
|
|
199
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(inflated);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
result: fail('invalid-link', 'The link data is not UTF-8 text.', 'Copy the whole link again.'),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { ok: true, text };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
result: fail('invalid-link', 'The link could not be read.', 'Copy the whole link again, or open the blueprint.json file.'),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Decodes an app share link payload (`1.<data>`, or a whole `#bp=` fragment)
|
|
218
|
+
* into a validated app blueprint or typed issues (`loadUntrustedBlueprint`).
|
|
219
|
+
* Never throws.
|
|
220
|
+
*/
|
|
221
|
+
export async function decodeBlueprintShare(input) {
|
|
222
|
+
const inflated = await inflateShare(input, MAX_SHARE_DECODED_BYTES);
|
|
223
|
+
return inflated.ok ? loadUntrustedBlueprint(inflated.text) : inflated.result;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Decodes a share link to a blueprint of any kind (app, deck, page, doc)
|
|
227
|
+
* through `loadUntrustedDocument`: content caps, multiline fields and the
|
|
228
|
+
* kind's body schema. Decompression stops at the document loader's byte cap.
|
|
229
|
+
* Never throws.
|
|
230
|
+
*/
|
|
231
|
+
export async function decodeDocumentShare(input) {
|
|
232
|
+
const inflated = await inflateShare(input, MAX_SHARE_DOCUMENT_DECODED_BYTES);
|
|
233
|
+
return inflated.ok ? loadUntrustedDocument(inflated.text) : inflated.result;
|
|
234
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The `sheet` screen: an entity's records as a spreadsheet (one row per
|
|
4
|
+
* record, one column per field), with a totals row that sums number, integer
|
|
5
|
+
* and money columns, CSV download, and cell edits saved through the entity's
|
|
6
|
+
* update service. Rendered by `@protoyard/crud/screens/sheet` over
|
|
7
|
+
* `@protoyard/views-ui/sheet`.
|
|
8
|
+
*
|
|
9
|
+
* Kept in its own module so the screen union in `schema.ts` only names it.
|
|
10
|
+
*/
|
|
11
|
+
/** Field kinds whose cells can be edited (typed text is checked by the entity schema on save). */
|
|
12
|
+
export declare const SHEET_EDITABLE_KINDS: readonly ["text", "longText", "number", "integer", "money", "boolean", "date", "enum", "email", "url", "phone"];
|
|
13
|
+
/** Field kinds a sheet shows read-only: their value is a label or a list, not one cell input. */
|
|
14
|
+
export declare const SHEET_READONLY_KINDS: readonly ["datetime", "relation", "persona", "tags"];
|
|
15
|
+
/** Every field kind a sheet column may name. */
|
|
16
|
+
export declare const SHEET_COLUMN_KINDS: ReadonlySet<string>;
|
|
17
|
+
/** Field kinds a totals row can sum. */
|
|
18
|
+
export declare const SHEET_TOTAL_KINDS: ReadonlySet<string>;
|
|
19
|
+
/** At most this many columns (A–Z). */
|
|
20
|
+
export declare const SHEET_MAX_COLUMNS = 26;
|
|
21
|
+
/**
|
|
22
|
+
* The screen's schema, built from `schema.ts`'s shared screen base and field
|
|
23
|
+
* name rule (passed in, so this module imports nothing from `schema.ts`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function sheetScreenSchema<Base extends z.ZodRawShape>(base: Base, fieldName: z.ZodString): z.ZodObject<Base & {
|
|
26
|
+
type: z.ZodLiteral<"sheet">;
|
|
27
|
+
entity: z.ZodString;
|
|
28
|
+
/** Fields shown as columns, in order; omitted means every column the entity can show. */
|
|
29
|
+
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
/** Number, integer or money columns summed in a totals row. */
|
|
31
|
+
totals: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
} extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strict>;
|
|
33
|
+
interface SheetFieldLike {
|
|
34
|
+
name: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
}
|
|
37
|
+
/** Cross-reference issues for one sheet screen of `entity` (`add(path, message)`). */
|
|
38
|
+
export declare function sheetScreenIssues(entity: {
|
|
39
|
+
name: string;
|
|
40
|
+
fields: readonly SheetFieldLike[];
|
|
41
|
+
}, screen: {
|
|
42
|
+
columns?: readonly string[] | undefined;
|
|
43
|
+
totals?: readonly string[] | undefined;
|
|
44
|
+
}, path: readonly (string | number)[], add: (path: (string | number)[], message: string) => void): void;
|
|
45
|
+
/** The columns a sheet shows: the listed ones, or every field a sheet can show (at most 26). */
|
|
46
|
+
export declare function sheetColumns<F extends SheetFieldLike>(entity: {
|
|
47
|
+
fields: readonly F[];
|
|
48
|
+
}, columns?: readonly string[]): F[];
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The `sheet` screen: an entity's records as a spreadsheet (one row per
|
|
4
|
+
* record, one column per field), with a totals row that sums number, integer
|
|
5
|
+
* and money columns, CSV download, and cell edits saved through the entity's
|
|
6
|
+
* update service. Rendered by `@protoyard/crud/screens/sheet` over
|
|
7
|
+
* `@protoyard/views-ui/sheet`.
|
|
8
|
+
*
|
|
9
|
+
* Kept in its own module so the screen union in `schema.ts` only names it.
|
|
10
|
+
*/
|
|
11
|
+
/** Field kinds whose cells can be edited (typed text is checked by the entity schema on save). */
|
|
12
|
+
export const SHEET_EDITABLE_KINDS = [
|
|
13
|
+
'text',
|
|
14
|
+
'longText',
|
|
15
|
+
'number',
|
|
16
|
+
'integer',
|
|
17
|
+
'money',
|
|
18
|
+
'boolean',
|
|
19
|
+
'date',
|
|
20
|
+
'enum',
|
|
21
|
+
'email',
|
|
22
|
+
'url',
|
|
23
|
+
'phone',
|
|
24
|
+
];
|
|
25
|
+
/** Field kinds a sheet shows read-only: their value is a label or a list, not one cell input. */
|
|
26
|
+
export const SHEET_READONLY_KINDS = ['datetime', 'relation', 'persona', 'tags'];
|
|
27
|
+
/** Every field kind a sheet column may name. */
|
|
28
|
+
export const SHEET_COLUMN_KINDS = new Set([
|
|
29
|
+
...SHEET_EDITABLE_KINDS,
|
|
30
|
+
...SHEET_READONLY_KINDS,
|
|
31
|
+
]);
|
|
32
|
+
/** Field kinds a totals row can sum. */
|
|
33
|
+
export const SHEET_TOTAL_KINDS = new Set(['number', 'integer', 'money']);
|
|
34
|
+
/** At most this many columns (A–Z). */
|
|
35
|
+
export const SHEET_MAX_COLUMNS = 26;
|
|
36
|
+
/**
|
|
37
|
+
* The screen's schema, built from `schema.ts`'s shared screen base and field
|
|
38
|
+
* name rule (passed in, so this module imports nothing from `schema.ts`).
|
|
39
|
+
*/
|
|
40
|
+
export function sheetScreenSchema(base, fieldName) {
|
|
41
|
+
return z.strictObject({
|
|
42
|
+
...base,
|
|
43
|
+
type: z.literal('sheet'),
|
|
44
|
+
entity: z.string().min(1),
|
|
45
|
+
/** Fields shown as columns, in order; omitted means every column the entity can show. */
|
|
46
|
+
columns: z.array(fieldName).min(1).max(SHEET_MAX_COLUMNS).optional(),
|
|
47
|
+
/** Number, integer or money columns summed in a totals row. */
|
|
48
|
+
totals: z.array(fieldName).max(SHEET_MAX_COLUMNS).optional(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Cross-reference issues for one sheet screen of `entity` (`add(path, message)`). */
|
|
52
|
+
export function sheetScreenIssues(entity, screen, path, add) {
|
|
53
|
+
const fields = new Map(entity.fields.map((field) => [field.name, field]));
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
screen.columns?.forEach((name, j) => {
|
|
56
|
+
const field = fields.get(name);
|
|
57
|
+
if (!field)
|
|
58
|
+
add([...path, 'columns', j], `unknown field "${name}" on "${entity.name}"`);
|
|
59
|
+
else if (!SHEET_COLUMN_KINDS.has(field.kind)) {
|
|
60
|
+
add([...path, 'columns', j], `a ${field.kind} field cannot be a sheet column ("${name}")`);
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(name))
|
|
63
|
+
add([...path, 'columns', j], `duplicate column "${name}"`);
|
|
64
|
+
seen.add(name);
|
|
65
|
+
});
|
|
66
|
+
const shown = new Set(screen.columns ?? sheetColumns(entity).map((field) => field.name));
|
|
67
|
+
screen.totals?.forEach((name, j) => {
|
|
68
|
+
const field = fields.get(name);
|
|
69
|
+
if (!field)
|
|
70
|
+
add([...path, 'totals', j], `unknown field "${name}" on "${entity.name}"`);
|
|
71
|
+
else if (!SHEET_TOTAL_KINDS.has(field.kind)) {
|
|
72
|
+
add([...path, 'totals', j], `totals "${name}" must be a number, integer or money field`);
|
|
73
|
+
}
|
|
74
|
+
else if (!shown.has(name)) {
|
|
75
|
+
add([...path, 'totals', j], `totals "${name}" is not one of the sheet's columns`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** The columns a sheet shows: the listed ones, or every field a sheet can show (at most 26). */
|
|
80
|
+
export function sheetColumns(entity, columns) {
|
|
81
|
+
if (columns) {
|
|
82
|
+
const byName = new Map(entity.fields.map((field) => [field.name, field]));
|
|
83
|
+
return columns.flatMap((name) => {
|
|
84
|
+
const field = byName.get(name);
|
|
85
|
+
return field ? [field] : [];
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return entity.fields
|
|
89
|
+
.filter((field) => SHEET_COLUMN_KINDS.has(field.kind))
|
|
90
|
+
.slice(0, SHEET_MAX_COLUMNS);
|
|
91
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IntegrityRules } from '@protoyard/core';
|
|
2
|
+
import { type Blueprint, type BlueprintEntity, type BlueprintField } from './schema.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Domain tables for optional entity features. They are ordinary stores (not
|
|
5
|
+
* `py_*`) so enabling a feature on a blueprint does not force a reserved-table
|
|
6
|
+
* migration on every existing prototype database.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FEATURE_TABLES: {
|
|
9
|
+
readonly audit: "auditEntries";
|
|
10
|
+
readonly files: "files";
|
|
11
|
+
readonly notifications: "notifications";
|
|
12
|
+
};
|
|
13
|
+
/** Dexie store strings for the feature tables (stable column order). */
|
|
14
|
+
export declare const FEATURE_STORE_STRINGS: Record<(typeof FEATURE_TABLES)[keyof typeof FEATURE_TABLES], string>;
|
|
15
|
+
/**
|
|
16
|
+
* Row property / Dexie index for a field that always gets a structural column
|
|
17
|
+
* (relations, personas, tags, files). Plain scalar fields are only indexed
|
|
18
|
+
* when listed in `indexes`, `titleField` or `defaultSort`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function storeIndexForField(field: BlueprintField): string | undefined;
|
|
21
|
+
/** Integrity / row property name for a relation or persona column (no `*`). */
|
|
22
|
+
export declare function relationProperty(field: BlueprintField): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Dexie store string for one entity table. A relation's declaring entity owns
|
|
25
|
+
* its foreign key; the inverse is discovered from target-side relation fields
|
|
26
|
+
* by the generated related-detail UI and does not create a second stored key.
|
|
27
|
+
* Column order is deterministic:
|
|
28
|
+
* `id`, structural field indexes in declaration order, then explicit
|
|
29
|
+
* indexes / title / defaultSort, then `createdAt`, `updatedAt`, and
|
|
30
|
+
* `deletedAt` when soft-delete is on.
|
|
31
|
+
*/
|
|
32
|
+
export declare function entityStoreString(entity: BlueprintEntity): string;
|
|
33
|
+
/**
|
|
34
|
+
* Dexie `stores` map for a blueprint (version-1 shape). Entity tables keep
|
|
35
|
+
* declaration order; feature tables are appended in a fixed name order so two
|
|
36
|
+
* parses of the same blueprint always yield byte-identical JSON.
|
|
37
|
+
*/
|
|
38
|
+
export declare function deriveStores(blueprint: Blueprint): Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* `IntegrityRules` matching `validateWorld`: every relation / persona / file
|
|
41
|
+
* id column, plus created/updated/(deleted) timestamps and date fields.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deriveIntegrity(blueprint: Blueprint): IntegrityRules;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { dateWindowDays, entityTable, } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Domain tables for optional entity features. They are ordinary stores (not
|
|
4
|
+
* `py_*`) so enabling a feature on a blueprint does not force a reserved-table
|
|
5
|
+
* migration on every existing prototype database.
|
|
6
|
+
*/
|
|
7
|
+
export const FEATURE_TABLES = {
|
|
8
|
+
audit: 'auditEntries',
|
|
9
|
+
files: 'files',
|
|
10
|
+
notifications: 'notifications',
|
|
11
|
+
};
|
|
12
|
+
/** Dexie store strings for the feature tables (stable column order). */
|
|
13
|
+
export const FEATURE_STORE_STRINGS = {
|
|
14
|
+
auditEntries: 'id, entity, recordId, actorId, action, at',
|
|
15
|
+
files: 'id, ownerId, entity, recordId, createdAt',
|
|
16
|
+
notifications: 'id, personaId, readAt, [personaId+readAt], createdAt',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Row property / Dexie index for a field that always gets a structural column
|
|
20
|
+
* (relations, personas, tags, files). Plain scalar fields are only indexed
|
|
21
|
+
* when listed in `indexes`, `titleField` or `defaultSort`.
|
|
22
|
+
*/
|
|
23
|
+
export function storeIndexForField(field) {
|
|
24
|
+
switch (field.kind) {
|
|
25
|
+
case 'relation':
|
|
26
|
+
return field.cardinality === 'many' ? `*${field.name}Ids` : `${field.name}Id`;
|
|
27
|
+
case 'persona':
|
|
28
|
+
return `${field.name}Id`;
|
|
29
|
+
case 'tags':
|
|
30
|
+
return `*${field.name}`;
|
|
31
|
+
case 'file':
|
|
32
|
+
return field.multiple ? `*${field.name}Ids` : `${field.name}Id`;
|
|
33
|
+
default:
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Integrity / row property name for a relation or persona column (no `*`). */
|
|
38
|
+
export function relationProperty(field) {
|
|
39
|
+
const index = storeIndexForField(field);
|
|
40
|
+
return index?.replace(/^\*/, '');
|
|
41
|
+
}
|
|
42
|
+
function indexForNamedField(entity, fieldName) {
|
|
43
|
+
const field = entity.fields.find((f) => f.name === fieldName);
|
|
44
|
+
if (!field)
|
|
45
|
+
return undefined;
|
|
46
|
+
return storeIndexForField(field) ?? fieldName;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Dexie store string for one entity table. A relation's declaring entity owns
|
|
50
|
+
* its foreign key; the inverse is discovered from target-side relation fields
|
|
51
|
+
* by the generated related-detail UI and does not create a second stored key.
|
|
52
|
+
* Column order is deterministic:
|
|
53
|
+
* `id`, structural field indexes in declaration order, then explicit
|
|
54
|
+
* indexes / title / defaultSort, then `createdAt`, `updatedAt`, and
|
|
55
|
+
* `deletedAt` when soft-delete is on.
|
|
56
|
+
*/
|
|
57
|
+
export function entityStoreString(entity) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const add = (index) => {
|
|
61
|
+
if (seen.has(index))
|
|
62
|
+
return;
|
|
63
|
+
seen.add(index);
|
|
64
|
+
parts.push(index);
|
|
65
|
+
};
|
|
66
|
+
add('id');
|
|
67
|
+
for (const field of entity.fields) {
|
|
68
|
+
const index = storeIndexForField(field);
|
|
69
|
+
if (index)
|
|
70
|
+
add(index);
|
|
71
|
+
}
|
|
72
|
+
for (const name of entity.indexes) {
|
|
73
|
+
const index = indexForNamedField(entity, name);
|
|
74
|
+
if (index)
|
|
75
|
+
add(index);
|
|
76
|
+
}
|
|
77
|
+
{
|
|
78
|
+
const index = indexForNamedField(entity, entity.titleField);
|
|
79
|
+
if (index)
|
|
80
|
+
add(index);
|
|
81
|
+
}
|
|
82
|
+
if (entity.defaultSort) {
|
|
83
|
+
const index = indexForNamedField(entity, entity.defaultSort.field);
|
|
84
|
+
if (index)
|
|
85
|
+
add(index);
|
|
86
|
+
}
|
|
87
|
+
add('createdAt');
|
|
88
|
+
add('updatedAt');
|
|
89
|
+
if (entity.features.softDelete)
|
|
90
|
+
add('deletedAt');
|
|
91
|
+
return parts.join(', ');
|
|
92
|
+
}
|
|
93
|
+
function featureFlags(blueprint) {
|
|
94
|
+
return {
|
|
95
|
+
audit: blueprint.entities.some((e) => e.features.audit),
|
|
96
|
+
files: blueprint.entities.some((e) => e.features.files),
|
|
97
|
+
notifications: blueprint.notifications.length > 0 ||
|
|
98
|
+
blueprint.entities.some((e) => e.features.notifications),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Dexie `stores` map for a blueprint (version-1 shape). Entity tables keep
|
|
103
|
+
* declaration order; feature tables are appended in a fixed name order so two
|
|
104
|
+
* parses of the same blueprint always yield byte-identical JSON.
|
|
105
|
+
*/
|
|
106
|
+
export function deriveStores(blueprint) {
|
|
107
|
+
const stores = {};
|
|
108
|
+
for (const entity of blueprint.entities) {
|
|
109
|
+
stores[entityTable(entity)] = entityStoreString(entity);
|
|
110
|
+
}
|
|
111
|
+
const flags = featureFlags(blueprint);
|
|
112
|
+
// Fixed order (alphabetical by table name) independent of which entity opted in.
|
|
113
|
+
if (flags.audit)
|
|
114
|
+
stores[FEATURE_TABLES.audit] = FEATURE_STORE_STRINGS.auditEntries;
|
|
115
|
+
if (flags.files)
|
|
116
|
+
stores[FEATURE_TABLES.files] = FEATURE_STORE_STRINGS.files;
|
|
117
|
+
if (flags.notifications) {
|
|
118
|
+
stores[FEATURE_TABLES.notifications] = FEATURE_STORE_STRINGS.notifications;
|
|
119
|
+
}
|
|
120
|
+
return stores;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* `IntegrityRules` matching `validateWorld`: every relation / persona / file
|
|
124
|
+
* id column, plus created/updated/(deleted) timestamps and date fields.
|
|
125
|
+
*/
|
|
126
|
+
export function deriveIntegrity(blueprint) {
|
|
127
|
+
const byName = new Map(blueprint.entities.map((e) => [e.name, e]));
|
|
128
|
+
const relations = {};
|
|
129
|
+
const timestamps = {};
|
|
130
|
+
for (const entity of blueprint.entities) {
|
|
131
|
+
const table = entityTable(entity);
|
|
132
|
+
const rels = {};
|
|
133
|
+
const times = ['createdAt', 'updatedAt'];
|
|
134
|
+
for (const field of entity.fields) {
|
|
135
|
+
if (field.kind === 'relation') {
|
|
136
|
+
const target = byName.get(field.to);
|
|
137
|
+
if (!target)
|
|
138
|
+
continue;
|
|
139
|
+
const property = relationProperty(field);
|
|
140
|
+
if (property)
|
|
141
|
+
rels[property] = entityTable(target);
|
|
142
|
+
}
|
|
143
|
+
else if (field.kind === 'persona') {
|
|
144
|
+
rels[`${field.name}Id`] = 'py_personas';
|
|
145
|
+
}
|
|
146
|
+
else if (field.kind === 'file') {
|
|
147
|
+
const property = relationProperty(field);
|
|
148
|
+
if (property)
|
|
149
|
+
rels[property] = FEATURE_TABLES.files;
|
|
150
|
+
}
|
|
151
|
+
else if (field.kind === 'date' || field.kind === 'datetime') {
|
|
152
|
+
// A window that reaches past now (due dates, bookings) is allowed in
|
|
153
|
+
// the future, so only its "not before the organization" bound applies
|
|
154
|
+
// through generation; it is left out of the reference-time check.
|
|
155
|
+
if (dateWindowDays(field.window).to <= 0)
|
|
156
|
+
times.push(field.name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (entity.features.softDelete)
|
|
160
|
+
times.push('deletedAt');
|
|
161
|
+
if (Object.keys(rels).length > 0)
|
|
162
|
+
relations[table] = rels;
|
|
163
|
+
timestamps[table] = times;
|
|
164
|
+
}
|
|
165
|
+
const flags = featureFlags(blueprint);
|
|
166
|
+
if (flags.audit) {
|
|
167
|
+
relations[FEATURE_TABLES.audit] = { actorId: 'py_personas' };
|
|
168
|
+
timestamps[FEATURE_TABLES.audit] = ['at'];
|
|
169
|
+
}
|
|
170
|
+
if (flags.files) {
|
|
171
|
+
relations[FEATURE_TABLES.files] = { ownerId: 'py_personas' };
|
|
172
|
+
timestamps[FEATURE_TABLES.files] = ['createdAt'];
|
|
173
|
+
}
|
|
174
|
+
if (flags.notifications) {
|
|
175
|
+
relations[FEATURE_TABLES.notifications] = { personaId: 'py_personas' };
|
|
176
|
+
timestamps[FEATURE_TABLES.notifications] = ['createdAt', 'readAt'];
|
|
177
|
+
}
|
|
178
|
+
return { relations, timestamps };
|
|
179
|
+
}
|