@protoyard/blueprint 0.0.0-stage → 1.1.0

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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1149 -2
  3. package/dist/aggregate.d.ts +83 -0
  4. package/dist/aggregate.js +268 -0
  5. package/dist/app-model.d.ts +76 -0
  6. package/dist/app-model.js +287 -0
  7. package/dist/archetypes/index.d.ts +1 -0
  8. package/dist/archetypes/index.js +1 -0
  9. package/dist/archetypes/schema.d.ts +126 -0
  10. package/dist/archetypes/schema.js +77 -0
  11. package/dist/audit.d.ts +52 -0
  12. package/dist/audit.js +149 -0
  13. package/dist/authoring/deck-lint.d.ts +32 -0
  14. package/dist/authoring/deck-lint.js +567 -0
  15. package/dist/authoring/index.d.ts +1 -0
  16. package/dist/authoring/index.js +1 -0
  17. package/dist/authoring/select-deck-layouts.d.ts +50 -0
  18. package/dist/authoring/select-deck-layouts.js +213 -0
  19. package/dist/authoring-apis.d.ts +13 -0
  20. package/dist/authoring-apis.js +13 -0
  21. package/dist/authoring-validation.d.ts +65 -0
  22. package/dist/authoring-validation.js +267 -0
  23. package/dist/avatar.d.ts +25 -0
  24. package/dist/avatar.js +30 -0
  25. package/dist/brand.d.ts +70 -0
  26. package/dist/brand.js +294 -0
  27. package/dist/brief/archetypes.d.ts +28 -0
  28. package/dist/brief/archetypes.js +798 -0
  29. package/dist/brief/content-synthesize.d.ts +31 -0
  30. package/dist/brief/content-synthesize.js +210 -0
  31. package/dist/brief/deck-outlines.d.ts +96 -0
  32. package/dist/brief/deck-outlines.js +561 -0
  33. package/dist/brief/draft-bodies.d.ts +29 -0
  34. package/dist/brief/draft-bodies.js +224 -0
  35. package/dist/brief/fields.d.ts +19 -0
  36. package/dist/brief/fields.js +229 -0
  37. package/dist/brief/index.d.ts +8 -0
  38. package/dist/brief/index.js +8 -0
  39. package/dist/brief/kind-detection.d.ts +69 -0
  40. package/dist/brief/kind-detection.js +353 -0
  41. package/dist/brief/limits.d.ts +117 -0
  42. package/dist/brief/limits.js +217 -0
  43. package/dist/brief/page-doc-outlines.d.ts +80 -0
  44. package/dist/brief/page-doc-outlines.js +485 -0
  45. package/dist/brief/synthesize.d.ts +59 -0
  46. package/dist/brief/synthesize.js +511 -0
  47. package/dist/brief/text.d.ts +13 -0
  48. package/dist/brief/text.js +59 -0
  49. package/dist/canvas-screen.d.ts +31 -0
  50. package/dist/canvas-screen.js +33 -0
  51. package/dist/canvas-seed.d.ts +58 -0
  52. package/dist/canvas-seed.js +82 -0
  53. package/dist/computed/evaluate.d.ts +11 -0
  54. package/dist/computed/evaluate.js +101 -0
  55. package/dist/computed/index.d.ts +4 -0
  56. package/dist/computed/index.js +3 -0
  57. package/dist/computed/public.d.ts +14 -0
  58. package/dist/computed/public.js +19 -0
  59. package/dist/computed/schema.d.ts +10 -0
  60. package/dist/computed/schema.js +140 -0
  61. package/dist/computed/types.d.ts +70 -0
  62. package/dist/computed/types.js +1 -0
  63. package/dist/computed/validate.d.ts +6 -0
  64. package/dist/computed/validate.js +146 -0
  65. package/dist/csv-guard.d.ts +15 -0
  66. package/dist/csv-guard.js +29 -0
  67. package/dist/csv.d.ts +85 -0
  68. package/dist/csv.js +288 -0
  69. package/dist/files.d.ts +35 -0
  70. package/dist/files.js +246 -0
  71. package/dist/form-schema.d.ts +124 -0
  72. package/dist/form-schema.js +300 -0
  73. package/dist/generator-version.d.ts +11 -0
  74. package/dist/generator-version.js +51 -0
  75. package/dist/generator.d.ts +34 -0
  76. package/dist/generator.js +445 -0
  77. package/dist/identity.d.ts +34 -0
  78. package/dist/identity.js +91 -0
  79. package/dist/import/csv-shape.d.ts +40 -0
  80. package/dist/import/csv-shape.js +265 -0
  81. package/dist/import/json-shape.d.ts +55 -0
  82. package/dist/import/json-shape.js +491 -0
  83. package/dist/import/limits.d.ts +13 -0
  84. package/dist/import/limits.js +14 -0
  85. package/dist/import/sqlite-schema.d.ts +36 -0
  86. package/dist/import/sqlite-schema.js +729 -0
  87. package/dist/index.d.ts +60 -0
  88. package/dist/index.js +58 -0
  89. package/dist/kinds/anchors.d.ts +18 -0
  90. package/dist/kinds/anchors.js +46 -0
  91. package/dist/kinds/bodies.d.ts +6 -0
  92. package/dist/kinds/bodies.js +32 -0
  93. package/dist/kinds/brand-runtime.d.ts +19 -0
  94. package/dist/kinds/brand-runtime.js +35 -0
  95. package/dist/kinds/deck.d.ts +8 -0
  96. package/dist/kinds/deck.js +34 -0
  97. package/dist/kinds/doc.d.ts +19 -0
  98. package/dist/kinds/doc.js +41 -0
  99. package/dist/kinds/embed/index.d.ts +39 -0
  100. package/dist/kinds/embed/index.js +91 -0
  101. package/dist/kinds/envelope.d.ts +159 -0
  102. package/dist/kinds/envelope.js +78 -0
  103. package/dist/kinds/index.d.ts +11 -0
  104. package/dist/kinds/index.js +11 -0
  105. package/dist/kinds/kind.d.ts +36 -0
  106. package/dist/kinds/kind.js +55 -0
  107. package/dist/kinds/limits.d.ts +64 -0
  108. package/dist/kinds/limits.js +190 -0
  109. package/dist/kinds/page-doc-runtime.d.ts +37 -0
  110. package/dist/kinds/page-doc-runtime.js +55 -0
  111. package/dist/kinds/page.d.ts +16 -0
  112. package/dist/kinds/page.js +67 -0
  113. package/dist/kinds/parse.d.ts +18 -0
  114. package/dist/kinds/parse.js +27 -0
  115. package/dist/kinds/runtime.d.ts +128 -0
  116. package/dist/kinds/runtime.js +206 -0
  117. package/dist/kinds/services.d.ts +49 -0
  118. package/dist/kinds/services.js +249 -0
  119. package/dist/kinds/testing.d.ts +9 -0
  120. package/dist/kinds/testing.js +16 -0
  121. package/dist/migrations.d.ts +97 -0
  122. package/dist/migrations.js +351 -0
  123. package/dist/motion.d.ts +50 -0
  124. package/dist/motion.js +52 -0
  125. package/dist/notifications.d.ts +19 -0
  126. package/dist/notifications.js +177 -0
  127. package/dist/patch.d.ts +122 -0
  128. package/dist/patch.js +322 -0
  129. package/dist/policy.d.ts +23 -0
  130. package/dist/policy.js +105 -0
  131. package/dist/runtime-definition.d.ts +39 -0
  132. package/dist/runtime-definition.js +74 -0
  133. package/dist/scenarios.d.ts +9 -0
  134. package/dist/scenarios.js +49 -0
  135. package/dist/schema/blueprint.schema.json +10191 -0
  136. package/dist/schema-json.d.ts +6 -0
  137. package/dist/schema-json.js +36 -0
  138. package/dist/schema.d.ts +6348 -0
  139. package/dist/schema.js +1269 -0
  140. package/dist/search.d.ts +16 -0
  141. package/dist/search.js +96 -0
  142. package/dist/services.d.ts +89 -0
  143. package/dist/services.js +898 -0
  144. package/dist/share.d.ts +78 -0
  145. package/dist/share.js +234 -0
  146. package/dist/sheet-screen.d.ts +49 -0
  147. package/dist/sheet-screen.js +91 -0
  148. package/dist/storage.d.ts +43 -0
  149. package/dist/storage.js +179 -0
  150. package/dist/stream.d.ts +26 -0
  151. package/dist/stream.js +173 -0
  152. package/dist/untrusted.d.ts +63 -0
  153. package/dist/untrusted.js +502 -0
  154. package/dist/validation-tasks.d.ts +178 -0
  155. package/dist/validation-tasks.js +199 -0
  156. package/dist/validation.d.ts +35 -0
  157. package/dist/validation.js +312 -0
  158. package/dist/variants.d.ts +112 -0
  159. package/dist/variants.js +283 -0
  160. package/dist/workflows/index.d.ts +3 -0
  161. package/dist/workflows/index.js +3 -0
  162. package/dist/workflows/schema.d.ts +50 -0
  163. package/dist/workflows/schema.js +34 -0
  164. package/dist/workflows/transition.d.ts +15 -0
  165. package/dist/workflows/transition.js +25 -0
  166. package/dist/workflows/validate.d.ts +17 -0
  167. package/dist/workflows/validate.js +140 -0
  168. package/package.json +120 -3
@@ -0,0 +1,83 @@
1
+ import { type Row } from '@protoyard/core';
2
+ import type { Aggregate, BlueprintEntity, BlueprintScreenFilter } from './schema.ts';
3
+ import type { EntityServices } from './services.ts';
4
+ /**
5
+ * Aggregates for dashboards, boards, analytics and calendars, computed in
6
+ * one pass over entity rows. Pure and framework-agnostic: screens wrap
7
+ * `createAggregateQuery` in `liveQuery`. Buckets are UTC (days start at
8
+ * midnight, weeks on Monday, months on the 1st), so results never depend on
9
+ * the viewer's time zone.
10
+ */
11
+ export type BucketUnit = 'day' | 'week' | 'month';
12
+ export interface AggregateSpec {
13
+ /** Default `count`. */
14
+ aggregate?: Aggregate;
15
+ /** Required for every aggregate except `count`. */
16
+ field?: string;
17
+ /** One series entry per value; array fields (tags, many-relations) count once per item. */
18
+ groupBy?: string;
19
+ /** Time buckets over a date or datetime field (or `createdAt` / `updatedAt`). */
20
+ bucket?: {
21
+ field: string;
22
+ unit: BucketUnit;
23
+ };
24
+ filter?: BlueprintScreenFilter;
25
+ /** Buckets to fill when bucketing (inclusive); defaults to the data's first and last bucket. */
26
+ range?: {
27
+ from: string | Date;
28
+ to: string | Date;
29
+ };
30
+ }
31
+ /** One cell: a bucket × group combination (either may be null when not used). */
32
+ export interface AggregateCell {
33
+ /** `<bucket|*>|<group|*>`, stable across runs. */
34
+ key: string;
35
+ /** Bucket start (ISO, UTC) or null without `bucket`. */
36
+ bucket: string | null;
37
+ /** Group value or null without `groupBy`; `NO_GROUP` collects rows without a value. */
38
+ group: string | null;
39
+ /** Rows that matched (after filter). */
40
+ count: number;
41
+ /** null for avg/min/max over no values. */
42
+ value: number | null;
43
+ }
44
+ export interface AggregateResult {
45
+ aggregate: Aggregate;
46
+ /**
47
+ * How to format `value`: `money` is integer cents, `date` is epoch
48
+ * milliseconds (min/max over date fields), `number` otherwise.
49
+ */
50
+ valueKind: 'number' | 'money' | 'date';
51
+ /** Over every matching row. */
52
+ total: AggregateCell;
53
+ /** Ordered: buckets ascending, then groups (enum order, else sorted, `NO_GROUP` last). */
54
+ series: AggregateCell[];
55
+ /** Group order used in `series`. */
56
+ groups: string[];
57
+ /** Bucket starts used in `series`, contiguous. */
58
+ buckets: string[];
59
+ }
60
+ /** Group key for rows whose groupBy value is empty. */
61
+ export declare const NO_GROUP = "__none__";
62
+ /** Upper bound on filled buckets, so a typo in a range cannot allocate millions of cells. */
63
+ export declare const MAX_BUCKETS = 5000;
64
+ /** UTC start of the bucket containing `ms`. */
65
+ export declare function bucketStartUtc(ms: number, unit: BucketUnit): number;
66
+ /**
67
+ * Aggregates `rows` (soft-deleted rows are skipped). Pass the blueprint
68
+ * entity so relation fields map to their `<name>Id` columns, enum groups keep
69
+ * their declared order (every value appears, even at zero), and money and
70
+ * dates are typed. Money sums stay integer cents; money and integer averages
71
+ * are rounded to integers.
72
+ */
73
+ export declare function aggregate(rows: readonly Row[], spec: AggregateSpec, entity?: BlueprintEntity): AggregateResult;
74
+ /** A spec bound to an entity, as dashboard widgets and analytics measures declare it. */
75
+ export type AggregateQuerySpec = AggregateSpec & {
76
+ entity: string;
77
+ };
78
+ /**
79
+ * A querier for `liveQuery`: reads through the entity service (so
80
+ * `session.assert(<entity>:read)` applies) and resolves to null when the
81
+ * active persona cannot read the entity, like the generated Home counts.
82
+ */
83
+ export declare function createAggregateQuery(services: EntityServices, spec: AggregateQuerySpec): () => Promise<AggregateResult | null>;
@@ -0,0 +1,268 @@
1
+ import { PermissionDeniedError } from '@protoyard/core';
2
+ import { inputProperty } from "./validation.js";
3
+ /** Group key for rows whose groupBy value is empty. */
4
+ export const NO_GROUP = '__none__';
5
+ /** Upper bound on filled buckets, so a typo in a range cannot allocate millions of cells. */
6
+ export const MAX_BUCKETS = 5000;
7
+ const MS_PER_DAY = 86_400_000;
8
+ const MANAGED_DATES = new Set(['createdAt', 'updatedAt']);
9
+ /** UTC start of the bucket containing `ms`. */
10
+ export function bucketStartUtc(ms, unit) {
11
+ const day = Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
12
+ if (unit === 'day')
13
+ return day;
14
+ if (unit === 'week')
15
+ return day - ((new Date(day).getUTCDay() + 6) % 7) * MS_PER_DAY;
16
+ const date = new Date(day);
17
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
18
+ }
19
+ function nextBucket(start, unit) {
20
+ if (unit === 'day')
21
+ return start + MS_PER_DAY;
22
+ if (unit === 'week')
23
+ return start + 7 * MS_PER_DAY;
24
+ const date = new Date(start);
25
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
26
+ }
27
+ /** Epoch ms for a date-like value; NaN when it is not one. */
28
+ function toMs(value) {
29
+ if (value instanceof Date)
30
+ return value.getTime();
31
+ return typeof value === 'string' && value !== '' ? Date.parse(value) : Number.NaN;
32
+ }
33
+ function fieldOf(entity, name) {
34
+ return entity?.fields.find((f) => f.name === name);
35
+ }
36
+ /** Row property that stores a field (`customer` → `customerId`). */
37
+ function propertyOf(entity, name) {
38
+ const field = fieldOf(entity, name);
39
+ return field ? inputProperty(field) : name;
40
+ }
41
+ function groupKeys(value) {
42
+ if (Array.isArray(value)) {
43
+ const keys = value.filter((v) => v !== null && v !== undefined && v !== '').map(String);
44
+ return keys.length > 0 ? [...new Set(keys)] : [NO_GROUP];
45
+ }
46
+ return value === null || value === undefined || value === '' ? [NO_GROUP] : [String(value)];
47
+ }
48
+ function makeFilter(entity, filter) {
49
+ if (!filter)
50
+ return () => true;
51
+ const prop = propertyOf(entity, filter.field);
52
+ const kind = MANAGED_DATES.has(filter.field) ? 'datetime' : fieldOf(entity, filter.field)?.kind;
53
+ const isDate = kind === 'date' || kind === 'datetime';
54
+ const comparable = (v) => {
55
+ if (v === null || v === undefined)
56
+ return null;
57
+ if (isDate)
58
+ return toMs(v);
59
+ return typeof v === 'number' || typeof v === 'boolean' ? v : String(v);
60
+ };
61
+ const wanted = (Array.isArray(filter.value) ? filter.value : [filter.value]).map(comparable);
62
+ const matchOne = (v) => {
63
+ const c = comparable(v);
64
+ if (c === null)
65
+ return false;
66
+ if (filter.op === 'eq' || filter.op === 'in')
67
+ return wanted.includes(c);
68
+ const bound = wanted[0];
69
+ if (bound === null || bound === undefined)
70
+ return false;
71
+ return filter.op === 'gte' ? c >= bound : c <= bound;
72
+ };
73
+ return (row) => {
74
+ const value = row[prop];
75
+ return Array.isArray(value) ? value.some(matchOne) : matchOne(value);
76
+ };
77
+ }
78
+ const newAcc = () => ({
79
+ count: 0,
80
+ n: 0,
81
+ sum: 0,
82
+ min: Number.POSITIVE_INFINITY,
83
+ max: Number.NEGATIVE_INFINITY,
84
+ });
85
+ function add(acc, value) {
86
+ acc.count++;
87
+ if (value === null)
88
+ return;
89
+ acc.n++;
90
+ acc.sum += value;
91
+ if (value < acc.min)
92
+ acc.min = value;
93
+ if (value > acc.max)
94
+ acc.max = value;
95
+ }
96
+ function finish(acc, aggregate, roundAvg) {
97
+ switch (aggregate) {
98
+ case 'count':
99
+ return acc.count;
100
+ case 'sum':
101
+ return acc.sum;
102
+ case 'avg':
103
+ if (acc.n === 0)
104
+ return null;
105
+ return roundAvg ? Math.round(acc.sum / acc.n) : acc.sum / acc.n;
106
+ case 'min':
107
+ return acc.n === 0 ? null : acc.min;
108
+ case 'max':
109
+ return acc.n === 0 ? null : acc.max;
110
+ }
111
+ }
112
+ /**
113
+ * Aggregates `rows` (soft-deleted rows are skipped). Pass the blueprint
114
+ * entity so relation fields map to their `<name>Id` columns, enum groups keep
115
+ * their declared order (every value appears, even at zero), and money and
116
+ * dates are typed. Money sums stay integer cents; money and integer averages
117
+ * are rounded to integers.
118
+ */
119
+ export function aggregate(rows, spec, entity) {
120
+ const kind = spec.aggregate ?? 'count';
121
+ if (kind !== 'count' && !spec.field) {
122
+ throw new RangeError(`aggregate(): "${kind}" needs a field`);
123
+ }
124
+ const valueField = spec.field ? fieldOf(entity, spec.field) : undefined;
125
+ const valueIsDate = valueField?.kind === 'date' ||
126
+ valueField?.kind === 'datetime' ||
127
+ (spec.field !== undefined && MANAGED_DATES.has(spec.field));
128
+ const valueKind = kind === 'count'
129
+ ? 'number'
130
+ : valueField?.kind === 'money'
131
+ ? 'money'
132
+ : valueIsDate
133
+ ? 'date'
134
+ : 'number';
135
+ const roundAvg = valueField?.kind === 'money' || valueField?.kind === 'integer';
136
+ const valueProp = spec.field ? propertyOf(entity, spec.field) : null;
137
+ const readValue = (row) => {
138
+ if (valueProp === null)
139
+ return null;
140
+ const raw = row[valueProp];
141
+ if (valueIsDate) {
142
+ const ms = toMs(raw);
143
+ return Number.isNaN(ms) ? null : ms;
144
+ }
145
+ return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;
146
+ };
147
+ const groupProp = spec.groupBy ? propertyOf(entity, spec.groupBy) : null;
148
+ const bucketProp = spec.bucket ? propertyOf(entity, spec.bucket.field) : null;
149
+ const unit = spec.bucket?.unit;
150
+ const keep = makeFilter(entity, spec.filter);
151
+ const total = newAcc();
152
+ const cells = new Map();
153
+ const seenGroups = new Set();
154
+ let firstBucket = Number.POSITIVE_INFINITY;
155
+ let lastBucket = Number.NEGATIVE_INFINITY;
156
+ for (const row of rows) {
157
+ if (row.deletedAt || !keep(row))
158
+ continue;
159
+ const value = readValue(row);
160
+ add(total, value);
161
+ let bucket = null;
162
+ if (bucketProp !== null && unit) {
163
+ const ms = toMs(row[bucketProp]);
164
+ if (Number.isNaN(ms))
165
+ continue; // no date: counted in total, not in a bucket
166
+ bucket = bucketStartUtc(ms, unit);
167
+ if (bucket < firstBucket)
168
+ firstBucket = bucket;
169
+ if (bucket > lastBucket)
170
+ lastBucket = bucket;
171
+ }
172
+ const groups = groupProp === null ? [null] : groupKeys(row[groupProp]);
173
+ for (const group of groups) {
174
+ if (group !== null)
175
+ seenGroups.add(group);
176
+ const key = `${bucket === null ? '*' : bucket}|${group ?? '*'}`;
177
+ let acc = cells.get(key);
178
+ if (!acc) {
179
+ acc = newAcc();
180
+ cells.set(key, acc);
181
+ }
182
+ add(acc, value);
183
+ }
184
+ }
185
+ // Group order: enum values as declared (all of them), then other seen values sorted.
186
+ let groups = [];
187
+ if (groupProp !== null) {
188
+ const declared = fieldOf(entity, spec.groupBy)?.kind === 'enum'
189
+ ? fieldOf(entity, spec.groupBy).values
190
+ : [];
191
+ const rest = [...seenGroups]
192
+ .filter((g) => g !== NO_GROUP && !declared.includes(g))
193
+ .sort((a, b) => a.localeCompare(b));
194
+ groups = [...declared, ...rest, ...(seenGroups.has(NO_GROUP) ? [NO_GROUP] : [])];
195
+ }
196
+ // Contiguous buckets over the range, or the data's span.
197
+ const buckets = [];
198
+ if (unit) {
199
+ let from = firstBucket;
200
+ let to = lastBucket;
201
+ if (spec.range) {
202
+ from = bucketStartUtc(toMs(spec.range.from), unit);
203
+ to = bucketStartUtc(toMs(spec.range.to), unit);
204
+ if (Number.isNaN(from) || Number.isNaN(to))
205
+ throw new RangeError('aggregate(): invalid range');
206
+ }
207
+ for (let b = from; b <= to; b = nextBucket(b, unit)) {
208
+ buckets.push(b);
209
+ if (buckets.length > MAX_BUCKETS) {
210
+ throw new RangeError(`aggregate(): more than ${MAX_BUCKETS} buckets`);
211
+ }
212
+ }
213
+ }
214
+ const cell = (bucket, group) => {
215
+ const key = `${bucket === null ? '*' : bucket}|${group ?? '*'}`;
216
+ const iso = bucket === null ? null : new Date(bucket).toISOString();
217
+ return {
218
+ key: `${iso ?? '*'}|${group ?? '*'}`,
219
+ bucket: iso,
220
+ group,
221
+ count: cells.get(key)?.count ?? 0,
222
+ value: finish(cells.get(key) ?? newAcc(), kind, roundAvg),
223
+ };
224
+ };
225
+ const series = [];
226
+ const bucketList = unit ? buckets : [null];
227
+ const groupList = groupProp === null ? [null] : groups;
228
+ for (const bucket of bucketList) {
229
+ for (const group of groupList)
230
+ series.push(cell(bucket, group));
231
+ }
232
+ return {
233
+ aggregate: kind,
234
+ valueKind,
235
+ total: {
236
+ key: '*|*',
237
+ bucket: null,
238
+ group: null,
239
+ count: total.count,
240
+ value: finish(total, kind, roundAvg),
241
+ },
242
+ series,
243
+ groups,
244
+ buckets: buckets.map((b) => new Date(b).toISOString()),
245
+ };
246
+ }
247
+ /**
248
+ * A querier for `liveQuery`: reads through the entity service (so
249
+ * `session.assert(<entity>:read)` applies) and resolves to null when the
250
+ * active persona cannot read the entity, like the generated Home counts.
251
+ */
252
+ export function createAggregateQuery(services, spec) {
253
+ const service = services[spec.entity];
254
+ if (!service)
255
+ throw new RangeError(`createAggregateQuery(): unknown entity "${spec.entity}"`);
256
+ return async () => {
257
+ let rows;
258
+ try {
259
+ rows = await service.list();
260
+ }
261
+ catch (error) {
262
+ if (error instanceof PermissionDeniedError)
263
+ return null;
264
+ throw error;
265
+ }
266
+ return aggregate(rows, spec, service.entity);
267
+ };
268
+ }
@@ -0,0 +1,76 @@
1
+ import type { LayoutId } from '@protoyard/core';
2
+ import { type Blueprint, type BlueprintEntity, type BlueprintScreen, type ScreenNavIcon, type ScreenType } from './schema.ts';
3
+ interface AppRouteBase {
4
+ /** Stable route name (`home`, `deal-list`, `screen-BoardDeal`, `members`). */
5
+ readonly name: string;
6
+ /** Hash route path, with `:param` segments. */
7
+ readonly path: string;
8
+ readonly title: string;
9
+ readonly description: string;
10
+ }
11
+ /**
12
+ * What a route renders:
13
+ * - `home`: the built-in overview with a count per entity;
14
+ * - `members`: workspace people and their simulated roles;
15
+ * - `entity`: the entity page (list, create and edit), optionally claimed by
16
+ * a `list`, `form` or tab-less `detail` screen;
17
+ * - `screen`: a blueprint screen with its own view component.
18
+ */
19
+ export type AppRoute = (AppRouteBase & {
20
+ readonly view: 'home';
21
+ }) | (AppRouteBase & {
22
+ readonly view: 'members';
23
+ }) | (AppRouteBase & {
24
+ readonly view: 'entity';
25
+ readonly entity: string;
26
+ readonly screenIndex?: number;
27
+ readonly screenType?: ScreenType;
28
+ }) | (AppRouteBase & {
29
+ readonly view: 'screen';
30
+ readonly screenIndex: number;
31
+ readonly screenType: ScreenType;
32
+ readonly entity?: string;
33
+ /** Unique component id for the screen (`Dashboard1Screen`, `BoardDealScreen`). */
34
+ readonly component: string;
35
+ });
36
+ /** What a route renders, without its name, path and labels. */
37
+ export type AppRouteTarget = AppRoute extends infer R ? R extends AppRoute ? Omit<R, keyof AppRouteBase> : never : never;
38
+ export interface AppNavItem {
39
+ readonly path: string;
40
+ readonly label: string;
41
+ readonly icon: ScreenNavIcon;
42
+ /** Active only on an exact path match (the home row). */
43
+ readonly exact?: true;
44
+ }
45
+ export interface AppNavGroup {
46
+ readonly label: string;
47
+ readonly paths: readonly string[];
48
+ }
49
+ export interface AppLayout {
50
+ readonly id: LayoutId;
51
+ readonly mobile?: LayoutId;
52
+ }
53
+ export interface AppModel {
54
+ /** Every route, in router order. */
55
+ readonly routes: readonly AppRoute[];
56
+ /** Sidebar rows: home, entities without a list screen, visible screens by nav order, members. */
57
+ readonly nav: readonly AppNavItem[];
58
+ /** Sectioned navigation (Overview, Records, Views, Workspace); empty groups are dropped. */
59
+ readonly navGroups: readonly AppNavGroup[];
60
+ readonly layout: AppLayout;
61
+ /** Every icon id the navigation may reference, in first-use order. */
62
+ readonly icons: readonly ScreenNavIcon[];
63
+ }
64
+ /** `support-tickets` → `Support tickets`, unless the entity sets `label`. */
65
+ export declare function entityLabel(entity: Pick<BlueprintEntity, 'label' | 'plural'>): string;
66
+ /** The icon a screen shows in navigation: its `nav.icon`, or the default for its kind. */
67
+ export declare function screenNavIcon(screen: BlueprintScreen): ScreenNavIcon;
68
+ /** True when a screen renders through its own component rather than the entity page. */
69
+ export declare function screenHasOwnView(screen: BlueprintScreen): boolean;
70
+ /**
71
+ * Derives the app model from a parsed blueprint. Pure and deterministic: the
72
+ * same blueprint always yields the same model, which matches what
73
+ * `protoyard generate` writes to `routes.ts` and `layout.ts`.
74
+ */
75
+ export declare function deriveAppModel(blueprint: Blueprint): AppModel;
76
+ export {};