@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.223

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 (135) hide show
  1. package/README.md +9 -6
  2. package/package.json +10 -10
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +1 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
  7. package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
  8. package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
  9. package/src/lib/artifact/dataset-schema.d.ts +136 -0
  10. package/src/lib/artifact/dataset-schema.js +232 -0
  11. package/src/lib/artifact/dataset-schema.js.map +1 -0
  12. package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
  13. package/src/lib/components/dataset-record-dialog.component.js +1 -1
  14. package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
  15. package/src/lib/components/dataset-record-filter.d.ts +1 -1
  16. package/src/lib/components/dataset-record-filter.js +2 -2
  17. package/src/lib/components/dataset-record-filter.js.map +1 -1
  18. package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
  19. package/src/lib/components/dataset-schema-dialog.component.js +55 -23
  20. package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
  21. package/src/lib/components/host-datasets-card.component.js +91 -403
  22. package/src/lib/components/host-datasets-card.component.js.map +1 -1
  23. package/src/lib/constants/bundle-common.d.ts +12 -0
  24. package/src/lib/constants/bundle-common.js +13 -0
  25. package/src/lib/constants/bundle-common.js.map +1 -1
  26. package/src/lib/declarations.console-server.d.ts +16 -1
  27. package/src/lib/declarations.console-server.js +34 -2
  28. package/src/lib/declarations.console-server.js.map +1 -1
  29. package/src/lib/declarations.server.d.ts +13 -0
  30. package/src/lib/declarations.server.js +48 -2
  31. package/src/lib/declarations.server.js.map +1 -1
  32. package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
  33. package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
  34. package/src/lib/model/dataset-models.d.ts +392 -0
  35. package/src/lib/model/dataset-models.js +806 -0
  36. package/src/lib/model/dataset-models.js.map +1 -0
  37. package/src/lib/model/dataset-record-view.d.ts +1 -1
  38. package/src/lib/model/dataset-record-view.js +1 -2
  39. package/src/lib/model/dataset-record-view.js.map +1 -1
  40. package/src/lib/model/dataset-record.js +2 -2
  41. package/src/lib/model/dataset-record.js.map +1 -1
  42. package/src/lib/model/datasets.d.ts +162 -0
  43. package/src/lib/model/datasets.js +227 -0
  44. package/src/lib/model/datasets.js.map +1 -0
  45. package/src/lib/plugin.d.ts +4 -0
  46. package/src/lib/plugin.js +40 -0
  47. package/src/lib/plugin.js.map +1 -1
  48. package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
  49. package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
  50. package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
  51. package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
  52. package/src/lib/record-pages/record-page-read.server.js +175 -0
  53. package/src/lib/record-pages/record-page-read.server.js.map +1 -0
  54. package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
  55. package/src/lib/record-pages/record-page-resolver.server.js +131 -0
  56. package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
  57. package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
  58. package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
  59. package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
  60. package/src/lib/record-pages/record-page-source.d.ts +42 -0
  61. package/src/lib/record-pages/record-page-source.js +176 -0
  62. package/src/lib/record-pages/record-page-source.js.map +1 -0
  63. package/src/lib/record-pages/record-pages-route.d.ts +42 -0
  64. package/src/lib/record-pages/record-pages-route.js +348 -0
  65. package/src/lib/record-pages/record-pages-route.js.map +1 -0
  66. package/src/lib/record-pages/record-pages.d.ts +218 -0
  67. package/src/lib/record-pages/record-pages.js +351 -0
  68. package/src/lib/record-pages/record-pages.js.map +1 -0
  69. package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
  70. package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
  71. package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
  72. package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
  73. package/src/lib/record-pages/record-template-section.component.js +491 -0
  74. package/src/lib/record-pages/record-template-section.component.js.map +1 -0
  75. package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
  76. package/src/lib/repeat/dataset-repeat-rows.js +30 -12
  77. package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
  78. package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
  79. package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
  80. package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
  81. package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
  82. package/src/lib/repeat/repeat-record-pages.js +64 -0
  83. package/src/lib/repeat/repeat-record-pages.js.map +1 -0
  84. package/src/lib/server/announce-dataset-records.d.ts +1 -1
  85. package/src/lib/server/announce-dataset-records.js +1 -1
  86. package/src/lib/server/announce-dataset-records.js.map +1 -1
  87. package/src/lib/server/api-v1/datasets.js +5 -3
  88. package/src/lib/server/api-v1/datasets.js.map +1 -1
  89. package/src/lib/server/dataset-figures.js +2 -2
  90. package/src/lib/server/dataset-figures.js.map +1 -1
  91. package/src/lib/server/dataset-live-pages.d.ts +147 -0
  92. package/src/lib/server/dataset-live-pages.js +436 -0
  93. package/src/lib/server/dataset-live-pages.js.map +1 -0
  94. package/src/lib/server/dataset-steps.server.d.ts +45 -0
  95. package/src/lib/server/dataset-steps.server.js +268 -0
  96. package/src/lib/server/dataset-steps.server.js.map +1 -0
  97. package/src/lib/server/datasets-route.d.ts +2 -0
  98. package/src/lib/server/datasets-route.js +102 -6
  99. package/src/lib/server/datasets-route.js.map +1 -1
  100. package/src/lib/server/resolve-dataset.d.ts +40 -0
  101. package/src/lib/server/resolve-dataset.js +64 -0
  102. package/src/lib/server/resolve-dataset.js.map +1 -0
  103. package/src/lib/server.d.ts +3 -2
  104. package/src/lib/server.js +8 -9
  105. package/src/lib/server.js.map +1 -1
  106. package/src/lib/site-bundle/datasets-package.d.ts +42 -0
  107. package/src/lib/site-bundle/datasets-package.js +95 -0
  108. package/src/lib/site-bundle/datasets-package.js.map +1 -0
  109. package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
  110. package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
  111. package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
  112. package/src/lib/tenant-api.js +30 -0
  113. package/src/lib/tenant-api.js.map +1 -0
  114. package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
  115. package/src/lib/transfer/dataset-transfer-key.js +31 -0
  116. package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
  117. package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
  118. package/src/lib/transfer/dataset-transfer-model.js +536 -0
  119. package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
  120. package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
  121. package/src/lib/transfer/dataset-transfer-resource.js +41 -0
  122. package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
  123. package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
  124. package/src/lib/transfer/dataset-transfer.server.js +930 -0
  125. package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
  126. package/src/lib/model/dataset-csv.d.ts +0 -36
  127. package/src/lib/model/dataset-csv.js +0 -85
  128. package/src/lib/model/dataset-csv.js.map +0 -1
  129. package/src/lib/model/dataset-io.d.ts +0 -44
  130. package/src/lib/model/dataset-io.js +0 -85
  131. package/src/lib/model/dataset-io.js.map +0 -1
  132. package/src/lib/model/index.js.map +0 -1
  133. package/src/lib/server/datasets-export-route.d.ts +0 -57
  134. package/src/lib/server/datasets-export-route.js +0 -229
  135. package/src/lib/server/datasets-export-route.js.map +0 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A dataset's schema as it travels between workspaces (AGL-657): what a
19
+ * publisher's dataset is reduced to, what an installing workspace's new
20
+ * dataset is made from, and what an update compares. The data plugin's,
21
+ * because only it knows a dataset's model (AGL-3080); the installer stores and
22
+ * diffs it as content it does not read.
23
+ *
24
+ * Pure and context-free, so the owner's server answers
25
+ * (`dataset-schema-artifact.server.ts`) and their specs share one shape.
26
+ */
27
+ /** Field cap on a published schema, mirroring the console's create limit. */
28
+ export declare const DATASET_SCHEMA_MAX_FIELDS = 100;
29
+ /** A published dataset schema: the model, with no records. */
30
+ export interface PublishedDatasetSchema {
31
+ fields: Record<string, PublishedDatasetField>;
32
+ order: string[];
33
+ }
34
+ export interface PublishedDatasetField {
35
+ name: string;
36
+ type: string;
37
+ customType?: string;
38
+ description?: string;
39
+ required?: boolean;
40
+ default?: unknown;
41
+ validation?: Record<string, unknown>;
42
+ /**
43
+ * Kept so an install into an org that HAS the referenced dataset can relink
44
+ * it. `datasetId` is the publisher's id and is meaningless in another org,
45
+ * so `datasetLabel` carries the human name the installer matches on.
46
+ */
47
+ reference?: {
48
+ datasetId?: string;
49
+ datasetLabel?: string;
50
+ displayFieldId?: string;
51
+ multiple?: boolean;
52
+ onDelete?: string;
53
+ };
54
+ }
55
+ /**
56
+ * Validates and strips a `DatasetModel` for publishing as a dataset schema
57
+ * (AGL-657).
58
+ *
59
+ * Publishes STRUCTURE ONLY: records never travel. That is the whole safety
60
+ * story for this artifact type: a dataset's rows are the org's customer data,
61
+ * so the publish path reads the model and nothing else, and there is no code
62
+ * path here that can reach the `records` subcollection.
63
+ *
64
+ * Two cross-org hazards the issue called out, handled by carrying enough
65
+ * context for the INSTALLER to decide rather than by rejecting at publish:
66
+ * - `reference` FKs point at a dataset id that only exists in the publisher's
67
+ * org. The id is kept alongside the referenced dataset's label so the
68
+ * installer can relink by name and degrade the field when it can't (see
69
+ * `resolveInstalledDatasetSchema`).
70
+ * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
71
+ * installing org may not have. Unknown custom types already degrade to their
72
+ * base type at render, so the name rides along untouched.
73
+ */
74
+ export declare function sanitizeDatasetSchema(model: {
75
+ fields?: Record<string, any>;
76
+ order?: unknown;
77
+ }): {
78
+ ok: true;
79
+ schema: PublishedDatasetSchema;
80
+ } | {
81
+ ok: false;
82
+ error: string;
83
+ };
84
+ /**
85
+ * Rewrites a published schema for the installing org (AGL-657).
86
+ *
87
+ * `reference` fields are the only part of a schema that can't cross an org
88
+ * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
89
+ * referenced dataset's label when the installing org has one by that name;
90
+ * otherwise degrade the field to plain `text` and report it, because a
91
+ * reference field pointing at a dataset that doesn't exist renders as a broken
92
+ * picker. Silently keeping the dead id would install something visibly
93
+ * broken and blame the installer for it.
94
+ *
95
+ * `existingDatasets` maps lowercased display name → dataset id in the target.
96
+ */
97
+ export declare function resolveInstalledDatasetSchema(schema: PublishedDatasetSchema, existingDatasets: Readonly<Record<string, string>>): {
98
+ schema: PublishedDatasetSchema;
99
+ degradedFieldIds: string[];
100
+ };
101
+ /**
102
+ * Dataset schemas, classified by what an update would do to EXISTING RECORDS
103
+ * (AGL-1018).
104
+ *
105
+ * The sharp edge of updating an installed copy: adding a field is inert, but
106
+ * removing or retyping one reinterprets data that is already there. So the
107
+ * schema plan is not "safe vs conflict" but "additive vs destructive", and
108
+ * destructive changes are never taken without a count of the records at stake.
109
+ */
110
+ export interface SchemaChangeSummary {
111
+ /** Field ids the new version adds. Applying these cannot harm a record. */
112
+ added: string[];
113
+ /** Field ids it removes: the values in existing records become orphaned. */
114
+ removed: string[];
115
+ /** Field ids whose TYPE changed: existing values may not survive a re-read. */
116
+ retyped: string[];
117
+ /** Anything else (labels, options, ordering). */
118
+ edited: string[];
119
+ /** True when nothing removes or retypes a field. */
120
+ additiveOnly: boolean;
121
+ }
122
+ interface SchemaShape {
123
+ order?: unknown;
124
+ fields?: Record<string, {
125
+ type?: unknown;
126
+ } | undefined>;
127
+ }
128
+ /**
129
+ * Compares two dataset schemas field by field.
130
+ *
131
+ * Deliberately ignores the base: for a schema the question is not "who edited
132
+ * this" but "what happens to the rows", and that is answered by the current
133
+ * schema against the incoming one regardless of who moved which field.
134
+ */
135
+ export declare function summarizeSchemaChange(current: SchemaShape | null | undefined, incoming: SchemaShape | null | undefined): SchemaChangeSummary;
136
+ export {};
@@ -0,0 +1,232 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { stableStringify } from "@aglyn/aglyn/app-utils/artifact-provenance";
19
+ import { PORTABLE_DATASET_FIELD_TYPES, PORTABLE_DEFINITION_MAX_BYTES } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
20
+ /**
21
+ * A dataset's schema as it travels between workspaces (AGL-657): what a
22
+ * publisher's dataset is reduced to, what an installing workspace's new
23
+ * dataset is made from, and what an update compares. The data plugin's,
24
+ * because only it knows a dataset's model (AGL-3080); the installer stores and
25
+ * diffs it as content it does not read.
26
+ *
27
+ * Pure and context-free, so the owner's server answers
28
+ * (`dataset-schema-artifact.server.ts`) and their specs share one shape.
29
+ */ /** Field cap on a published schema, mirroring the console's create limit. */ export const DATASET_SCHEMA_MAX_FIELDS = 100;
30
+ const KEPT_VALIDATION_KEYS = [
31
+ 'required',
32
+ 'regex',
33
+ 'min',
34
+ 'max',
35
+ 'options'
36
+ ];
37
+ /**
38
+ * Validates and strips a `DatasetModel` for publishing as a dataset schema
39
+ * (AGL-657).
40
+ *
41
+ * Publishes STRUCTURE ONLY: records never travel. That is the whole safety
42
+ * story for this artifact type: a dataset's rows are the org's customer data,
43
+ * so the publish path reads the model and nothing else, and there is no code
44
+ * path here that can reach the `records` subcollection.
45
+ *
46
+ * Two cross-org hazards the issue called out, handled by carrying enough
47
+ * context for the INSTALLER to decide rather than by rejecting at publish:
48
+ * - `reference` FKs point at a dataset id that only exists in the publisher's
49
+ * org. The id is kept alongside the referenced dataset's label so the
50
+ * installer can relink by name and degrade the field when it can't (see
51
+ * `resolveInstalledDatasetSchema`).
52
+ * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
53
+ * installing org may not have. Unknown custom types already degrade to their
54
+ * base type at render, so the name rides along untouched.
55
+ */ export function sanitizeDatasetSchema(model) {
56
+ const fields = model == null ? void 0 : model.fields;
57
+ if (!fields || typeof fields !== 'object') {
58
+ return {
59
+ ok: false,
60
+ error: 'Dataset has no field model to publish'
61
+ };
62
+ }
63
+ // Order is the display contract; fall back to key order for models written
64
+ // before `order` was required so older datasets stay publishable.
65
+ const order = Array.isArray(model.order) ? model.order.map(String) : Object.keys(fields);
66
+ const kept = order.filter((id)=>fields[id]);
67
+ if (!kept.length) {
68
+ return {
69
+ ok: false,
70
+ error: 'Dataset has no fields to publish'
71
+ };
72
+ }
73
+ if (kept.length > DATASET_SCHEMA_MAX_FIELDS) {
74
+ return {
75
+ ok: false,
76
+ error: `Dataset schemas are limited to ${DATASET_SCHEMA_MAX_FIELDS} fields`
77
+ };
78
+ }
79
+ const schema = {
80
+ fields: {},
81
+ order: kept
82
+ };
83
+ for (const id of kept){
84
+ var _fields_id, _field_type, _field_name;
85
+ const field = (_fields_id = fields[id]) != null ? _fields_id : {};
86
+ const type = String((_field_type = field.type) != null ? _field_type : 'text');
87
+ if (!PORTABLE_DATASET_FIELD_TYPES.includes(type)) {
88
+ return {
89
+ ok: false,
90
+ error: `Field "${id}" has an unsupported type`
91
+ };
92
+ }
93
+ const safe = {
94
+ name: String((_field_name = field.name) != null ? _field_name : id).slice(0, 120),
95
+ type
96
+ };
97
+ if (field.customType) safe.customType = String(field.customType).slice(0, 60);
98
+ if (field.description) {
99
+ safe.description = String(field.description).slice(0, 500);
100
+ }
101
+ if (field.required === true) safe.required = true;
102
+ // Defaults are publisher-authored values, not data. Keep only primitives
103
+ // so a default can't smuggle a nested object into the installing org.
104
+ if (field.default !== undefined && (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean')) {
105
+ safe.default = field.default;
106
+ }
107
+ if (field.validation && typeof field.validation === 'object') {
108
+ const validation = {};
109
+ for (const key of KEPT_VALIDATION_KEYS){
110
+ if (field.validation[key] !== undefined) {
111
+ validation[key] = field.validation[key];
112
+ }
113
+ }
114
+ if (Object.keys(validation).length) safe.validation = validation;
115
+ }
116
+ if (field.reference && typeof field.reference === 'object') {
117
+ safe.reference = _extends({}, field.reference.datasetId && {
118
+ datasetId: String(field.reference.datasetId)
119
+ }, field.reference.datasetLabel && {
120
+ datasetLabel: String(field.reference.datasetLabel).slice(0, 120)
121
+ }, field.reference.displayFieldId && {
122
+ displayFieldId: String(field.reference.displayFieldId)
123
+ }, field.reference.multiple === true && {
124
+ multiple: true
125
+ }, field.reference.onDelete && {
126
+ onDelete: String(field.reference.onDelete)
127
+ });
128
+ }
129
+ schema.fields[id] = safe;
130
+ }
131
+ const serialized = JSON.stringify(schema);
132
+ if (serialized.length > PORTABLE_DEFINITION_MAX_BYTES) {
133
+ return {
134
+ ok: false,
135
+ error: 'Dataset schema is too large to publish'
136
+ };
137
+ }
138
+ return {
139
+ ok: true,
140
+ schema
141
+ };
142
+ }
143
+ /**
144
+ * Rewrites a published schema for the installing org (AGL-657).
145
+ *
146
+ * `reference` fields are the only part of a schema that can't cross an org
147
+ * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
148
+ * referenced dataset's label when the installing org has one by that name;
149
+ * otherwise degrade the field to plain `text` and report it, because a
150
+ * reference field pointing at a dataset that doesn't exist renders as a broken
151
+ * picker. Silently keeping the dead id would install something visibly
152
+ * broken and blame the installer for it.
153
+ *
154
+ * `existingDatasets` maps lowercased display name → dataset id in the target.
155
+ */ export function resolveInstalledDatasetSchema(schema, existingDatasets) {
156
+ const degradedFieldIds = [];
157
+ const fields = {};
158
+ for (const id of schema.order){
159
+ var _ref;
160
+ var _field_reference;
161
+ const field = schema.fields[id];
162
+ if (!field) continue;
163
+ if (field.type !== 'reference' && !field.reference) {
164
+ fields[id] = field;
165
+ continue;
166
+ }
167
+ const label = String((_ref = (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetLabel) != null ? _ref : '').toLowerCase();
168
+ const relinked = label ? existingDatasets[label] : undefined;
169
+ if (relinked) {
170
+ fields[id] = _extends({}, field, {
171
+ reference: _extends({}, field.reference, {
172
+ datasetId: relinked
173
+ })
174
+ });
175
+ continue;
176
+ }
177
+ const { reference: _dropped } = field, rest = _object_without_properties_loose(field, [
178
+ "reference"
179
+ ]);
180
+ fields[id] = _extends({}, rest, {
181
+ type: 'text'
182
+ });
183
+ degradedFieldIds.push(id);
184
+ }
185
+ return {
186
+ schema: {
187
+ fields,
188
+ order: schema.order
189
+ },
190
+ degradedFieldIds
191
+ };
192
+ }
193
+ const same = (a, b)=>stableStringify(a) === stableStringify(b);
194
+ /**
195
+ * Compares two dataset schemas field by field.
196
+ *
197
+ * Deliberately ignores the base: for a schema the question is not "who edited
198
+ * this" but "what happens to the rows", and that is answered by the current
199
+ * schema against the incoming one regardless of who moved which field.
200
+ */ export function summarizeSchemaChange(current, incoming) {
201
+ var _ref, _ref1;
202
+ const currentFields = (_ref = current == null ? void 0 : current.fields) != null ? _ref : {};
203
+ const incomingFields = (_ref1 = incoming == null ? void 0 : incoming.fields) != null ? _ref1 : {};
204
+ const added = [];
205
+ const removed = [];
206
+ const retyped = [];
207
+ const edited = [];
208
+ for (const id of Object.keys(incomingFields)){
209
+ var _currentFields_id, _incomingFields_id;
210
+ if (!(id in currentFields)) {
211
+ added.push(id);
212
+ continue;
213
+ }
214
+ if (String((_currentFields_id = currentFields[id]) == null ? void 0 : _currentFields_id.type) !== String((_incomingFields_id = incomingFields[id]) == null ? void 0 : _incomingFields_id.type)) {
215
+ retyped.push(id);
216
+ } else if (!same(currentFields[id], incomingFields[id])) {
217
+ edited.push(id);
218
+ }
219
+ }
220
+ for (const id of Object.keys(currentFields)){
221
+ if (!(id in incomingFields)) removed.push(id);
222
+ }
223
+ return {
224
+ added,
225
+ removed,
226
+ retyped,
227
+ edited,
228
+ additiveOnly: !removed.length && !retyped.length
229
+ };
230
+ }
231
+
232
+ //# sourceMappingURL=dataset-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/artifact/dataset-schema.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { stableStringify } from '@aglyn/aglyn/app-utils/artifact-provenance'\nimport {\n PORTABLE_DATASET_FIELD_TYPES,\n PORTABLE_DEFINITION_MAX_BYTES,\n} from '@aglyn/aglyn/app-utils/node-definition-sanitizer'\n\n/**\n * A dataset's schema as it travels between workspaces (AGL-657): what a\n * publisher's dataset is reduced to, what an installing workspace's new\n * dataset is made from, and what an update compares. The data plugin's,\n * because only it knows a dataset's model (AGL-3080); the installer stores and\n * diffs it as content it does not read.\n *\n * Pure and context-free, so the owner's server answers\n * (`dataset-schema-artifact.server.ts`) and their specs share one shape.\n */\n\n/** Field cap on a published schema, mirroring the console's create limit. */\nexport const DATASET_SCHEMA_MAX_FIELDS = 100\n\n/** A published dataset schema: the model, with no records. */\nexport interface PublishedDatasetSchema {\n fields: Record<string, PublishedDatasetField>\n order: string[]\n}\n\nexport interface PublishedDatasetField {\n name: string\n type: string\n customType?: string\n description?: string\n required?: boolean\n default?: unknown\n validation?: Record<string, unknown>\n /**\n * Kept so an install into an org that HAS the referenced dataset can relink\n * it. `datasetId` is the publisher's id and is meaningless in another org,\n * so `datasetLabel` carries the human name the installer matches on.\n */\n reference?: {\n datasetId?: string\n datasetLabel?: string\n displayFieldId?: string\n multiple?: boolean\n onDelete?: string\n }\n}\n\nconst KEPT_VALIDATION_KEYS = ['required', 'regex', 'min', 'max', 'options']\n\n/**\n * Validates and strips a `DatasetModel` for publishing as a dataset schema\n * (AGL-657).\n *\n * Publishes STRUCTURE ONLY: records never travel. That is the whole safety\n * story for this artifact type: a dataset's rows are the org's customer data,\n * so the publish path reads the model and nothing else, and there is no code\n * path here that can reach the `records` subcollection.\n *\n * Two cross-org hazards the issue called out, handled by carrying enough\n * context for the INSTALLER to decide rather than by rejecting at publish:\n * - `reference` FKs point at a dataset id that only exists in the publisher's\n * org. The id is kept alongside the referenced dataset's label so the\n * installer can relink by name and degrade the field when it can't (see\n * `resolveInstalledDatasetSchema`).\n * - `customType` names a plugin-declared field type (AGL-434) whose plugin the\n * installing org may not have. Unknown custom types already degrade to their\n * base type at render, so the name rides along untouched.\n */\nexport function sanitizeDatasetSchema(model: {\n fields?: Record<string, any>\n order?: unknown\n}):\n | { ok: true; schema: PublishedDatasetSchema }\n | { ok: false; error: string } {\n const fields = model?.fields\n if (!fields || typeof fields !== 'object') {\n return { ok: false, error: 'Dataset has no field model to publish' }\n }\n // Order is the display contract; fall back to key order for models written\n // before `order` was required so older datasets stay publishable.\n const order = Array.isArray(model.order)\n ? model.order.map(String)\n : Object.keys(fields)\n const kept = order.filter((id) => fields[id])\n if (!kept.length) {\n return { ok: false, error: 'Dataset has no fields to publish' }\n }\n if (kept.length > DATASET_SCHEMA_MAX_FIELDS) {\n return {\n ok: false,\n error: `Dataset schemas are limited to ${DATASET_SCHEMA_MAX_FIELDS} fields`,\n }\n }\n const schema: PublishedDatasetSchema = { fields: {}, order: kept }\n for (const id of kept) {\n const field = fields[id] ?? {}\n const type = String(field.type ?? 'text')\n if (!PORTABLE_DATASET_FIELD_TYPES.includes(type)) {\n return { ok: false, error: `Field \"${id}\" has an unsupported type` }\n }\n const safe: PublishedDatasetField = {\n name: String(field.name ?? id).slice(0, 120),\n type,\n }\n if (field.customType) safe.customType = String(field.customType).slice(0, 60)\n if (field.description) {\n safe.description = String(field.description).slice(0, 500)\n }\n if (field.required === true) safe.required = true\n // Defaults are publisher-authored values, not data. Keep only primitives\n // so a default can't smuggle a nested object into the installing org.\n if (\n field.default !== undefined &&\n (typeof field.default === 'string' ||\n typeof field.default === 'number' ||\n typeof field.default === 'boolean')\n ) {\n safe.default = field.default\n }\n if (field.validation && typeof field.validation === 'object') {\n const validation: Record<string, unknown> = {}\n for (const key of KEPT_VALIDATION_KEYS) {\n if (field.validation[key] !== undefined) {\n validation[key] = field.validation[key]\n }\n }\n if (Object.keys(validation).length) safe.validation = validation\n }\n if (field.reference && typeof field.reference === 'object') {\n safe.reference = {\n ...(field.reference.datasetId && {\n datasetId: String(field.reference.datasetId),\n }),\n ...(field.reference.datasetLabel && {\n datasetLabel: String(field.reference.datasetLabel).slice(0, 120),\n }),\n ...(field.reference.displayFieldId && {\n displayFieldId: String(field.reference.displayFieldId),\n }),\n ...(field.reference.multiple === true && { multiple: true }),\n ...(field.reference.onDelete && {\n onDelete: String(field.reference.onDelete),\n }),\n }\n }\n schema.fields[id] = safe\n }\n const serialized = JSON.stringify(schema)\n if (serialized.length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Dataset schema is too large to publish' }\n }\n return { ok: true, schema }\n}\n\n/**\n * Rewrites a published schema for the installing org (AGL-657).\n *\n * `reference` fields are the only part of a schema that can't cross an org\n * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the\n * referenced dataset's label when the installing org has one by that name;\n * otherwise degrade the field to plain `text` and report it, because a\n * reference field pointing at a dataset that doesn't exist renders as a broken\n * picker. Silently keeping the dead id would install something visibly\n * broken and blame the installer for it.\n *\n * `existingDatasets` maps lowercased display name → dataset id in the target.\n */\nexport function resolveInstalledDatasetSchema(\n schema: PublishedDatasetSchema,\n existingDatasets: Readonly<Record<string, string>>,\n): { schema: PublishedDatasetSchema; degradedFieldIds: string[] } {\n const degradedFieldIds: string[] = []\n const fields: Record<string, PublishedDatasetField> = {}\n for (const id of schema.order) {\n const field = schema.fields[id]\n if (!field) continue\n if (field.type !== 'reference' && !field.reference) {\n fields[id] = field\n continue\n }\n const label = String(field.reference?.datasetLabel ?? '').toLowerCase()\n const relinked = label ? existingDatasets[label] : undefined\n if (relinked) {\n fields[id] = {\n ...field,\n reference: { ...field.reference, datasetId: relinked },\n }\n continue\n }\n const { reference: _dropped, ...rest } = field\n fields[id] = { ...rest, type: 'text' }\n degradedFieldIds.push(id)\n }\n return { schema: { fields, order: schema.order }, degradedFieldIds }\n}\n\n/**\n * Dataset schemas, classified by what an update would do to EXISTING RECORDS\n * (AGL-1018).\n *\n * The sharp edge of updating an installed copy: adding a field is inert, but\n * removing or retyping one reinterprets data that is already there. So the\n * schema plan is not \"safe vs conflict\" but \"additive vs destructive\", and\n * destructive changes are never taken without a count of the records at stake.\n */\nexport interface SchemaChangeSummary {\n /** Field ids the new version adds. Applying these cannot harm a record. */\n added: string[]\n /** Field ids it removes: the values in existing records become orphaned. */\n removed: string[]\n /** Field ids whose TYPE changed: existing values may not survive a re-read. */\n retyped: string[]\n /** Anything else (labels, options, ordering). */\n edited: string[]\n /** True when nothing removes or retypes a field. */\n additiveOnly: boolean\n}\n\ninterface SchemaShape {\n order?: unknown\n fields?: Record<string, { type?: unknown } | undefined>\n}\n\nconst same = (a: unknown, b: unknown): boolean =>\n stableStringify(a) === stableStringify(b)\n\n/**\n * Compares two dataset schemas field by field.\n *\n * Deliberately ignores the base: for a schema the question is not \"who edited\n * this\" but \"what happens to the rows\", and that is answered by the current\n * schema against the incoming one regardless of who moved which field.\n */\nexport function summarizeSchemaChange(\n current: SchemaShape | null | undefined,\n incoming: SchemaShape | null | undefined,\n): SchemaChangeSummary {\n const currentFields = (current?.fields ?? {}) as Record<string, { type?: unknown }>\n const incomingFields = (incoming?.fields ?? {}) as Record<string, { type?: unknown }>\n const added: string[] = []\n const removed: string[] = []\n const retyped: string[] = []\n const edited: string[] = []\n for (const id of Object.keys(incomingFields)) {\n if (!(id in currentFields)) {\n added.push(id)\n continue\n }\n if (String(currentFields[id]?.type) !== String(incomingFields[id]?.type)) {\n retyped.push(id)\n } else if (!same(currentFields[id], incomingFields[id])) {\n edited.push(id)\n }\n }\n for (const id of Object.keys(currentFields)) {\n if (!(id in incomingFields)) removed.push(id)\n }\n return {\n added,\n removed,\n retyped,\n edited,\n additiveOnly: !removed.length && !retyped.length,\n }\n}\n"],"names":["stableStringify","PORTABLE_DATASET_FIELD_TYPES","PORTABLE_DEFINITION_MAX_BYTES","DATASET_SCHEMA_MAX_FIELDS","KEPT_VALIDATION_KEYS","sanitizeDatasetSchema","model","fields","ok","error","order","Array","isArray","map","String","Object","keys","kept","filter","id","length","schema","field","type","includes","safe","name","slice","customType","description","required","default","undefined","validation","key","reference","datasetId","datasetLabel","displayFieldId","multiple","onDelete","serialized","JSON","stringify","resolveInstalledDatasetSchema","existingDatasets","degradedFieldIds","label","toLowerCase","relinked","_dropped","rest","push","same","a","b","summarizeSchemaChange","current","incoming","currentFields","incomingFields","added","removed","retyped","edited","additiveOnly"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { type DatasetModel } from '@aglyn/aglyn';
2
1
  import { type DatasetReferenceResolver } from '../model/dataset-record-view';
2
+ import { type DatasetModel } from '../model/dataset-models';
3
3
  /**
4
4
  * Looking at a record, as its own gesture.
5
5
  *
@@ -16,10 +16,10 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { validateDocument } from "@aglyn/aglyn";
20
19
  import { datasetRecordFields } from "../model/dataset-record-view.js";
21
20
  import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Tooltip, Typography } from "@mui/material";
22
21
  import { useMemo } from "react";
22
+ import { validateDocument } from "../model/dataset-models.js";
23
23
  /** What each "no value" state is called on screen. */ const PLACEHOLDER = {
24
24
  absent: 'Not set',
25
25
  null: 'Null',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/components/dataset-record-dialog.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { type DatasetModel, validateDocument } from '@aglyn/aglyn'\nimport {\n type DatasetRecordField,\n type DatasetReferenceResolver,\n datasetRecordFields,\n} from '../model/dataset-record-view'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useMemo } from 'react'\n\n/**\n * Looking at a record, as its own gesture.\n *\n * The records table has room for the model's columns and one line per value,\n * so the only way to see what a record actually holds was to open it in the\n * EDITOR — which made reading a record and changing one the same click, on a\n * dialog whose primary button writes. This is the other half: the whole\n * record, nothing writable.\n *\n * ## It reads nothing\n *\n * Every value on screen arrives as a prop, out of the page the table already\n * listens to. There is no query, no `getDoc`, and no effect — opening a record\n * costs zero documents, and rendering the table does not open one dialog per\n * row to find out. That is not an optimization detail: a viewer that fetched\n * the record it was handed would bill a read per glance, and one that mounted\n * per row would bill the whole page on arrival.\n *\n * ## Read-only means read-only\n *\n * Nothing here takes input and nothing here writes. `Edit record` is a\n * separate, explicitly labelled action that closes this and opens the editor —\n * the dialog that has always owned writing — so the two gestures stay\n * distinct rather than one being a slip away from the other.\n */\nexport interface DatasetRecordDialogProps {\n /**\n * The record row itself, straight from the table's page. `null` closes the\n * dialog — including when the row leaves the page under it, which is the\n * honest response to a record this reader can no longer see.\n */\n record: { $id: string; values?: Record<string, unknown> } | null\n model: DatasetModel\n /** Reference target ID → label, or `null` when it resolves to nothing. */\n resolveReference?: DatasetReferenceResolver\n onClose: () => void\n /**\n * Opens the record in the editor. Omitted for a reader who cannot write, so\n * the control is absent rather than present-and-refusing.\n */\n onEdit?: () => void\n}\n\n/** What each \"no value\" state is called on screen. */\nconst PLACEHOLDER: Record<string, string> = {\n absent: 'Not set',\n null: 'Null',\n 'empty-text': 'Empty text',\n 'empty-list': 'Empty list',\n 'empty-map': 'Empty map',\n}\n\n/**\n * One field's value.\n *\n * The four empty states get four different words, deliberately. A table cell\n * prints `--` for all of them because it has one line to work with; here there\n * is room to say which, and the difference decides what a reader does next —\n * a field that was never written and a field holding a real empty string are\n * not the same finding.\n */\nfunction RecordFieldValue(props: { field: DatasetRecordField }) {\n const { value } = props.field\n if (value.kind === 'opaque') {\n return (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ fontStyle: 'italic' }}\n >\n {value.text}\n </Typography>\n )\n }\n if (value.kind !== 'value') {\n return (\n <Typography\n variant=\"body2\"\n color=\"text.disabled\"\n sx={{ fontStyle: 'italic' }}\n >\n {PLACEHOLDER[value.kind] ?? 'Not set'}\n </Typography>\n )\n }\n if (value.references) {\n return (\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {value.references.map((reference) => (\n <Typography\n key={reference.id}\n variant=\"body2\"\n // An unresolved target is a fact about the data, not a rendering\n // failure to paper over: the ID is shown either way, and only the\n // resolved one is allowed to look like a working link.\n color={reference.label ? 'text.primary' : 'warning.main'}\n sx={{ overflowWrap: 'anywhere' }}\n >\n {reference.label ?? `${reference.id} · unresolved`}\n </Typography>\n ))}\n </Stack>\n )\n }\n if (value.items) {\n return (\n // One line per item. The comma-joined form a cell shows cannot say\n // whether `a, b` is one item or two; a list of lines can.\n <Stack component=\"ul\" spacing={0.25} sx={{ m: 0, pl: 2.5 }}>\n {value.items.map((item, index) => (\n <Typography\n // Items are values, not identities — two equal strings are two\n // real entries and the position is what tells them apart.\n key={`${index}:${item}`}\n component=\"li\"\n variant=\"body2\"\n sx={{ overflowWrap: 'anywhere' }}\n >\n {item}\n </Typography>\n ))}\n </Stack>\n )\n }\n return (\n <Typography\n variant=\"body2\"\n // Pretty-printed JSON is only readable where its whitespace survives.\n {...(value.block\n ? {\n component: 'pre',\n sx: {\n m: 0,\n fontFamily: 'monospace',\n whiteSpace: 'pre-wrap',\n overflowWrap: 'anywhere',\n },\n }\n : { sx: { overflowWrap: 'anywhere' } })}\n >\n {value.text}\n </Typography>\n )\n}\nRecordFieldValue.displayName = 'RecordFieldValue'\n\nexport function DatasetRecordDialog(props: DatasetRecordDialogProps) {\n const { record, model, resolveReference, onClose, onEdit } = props\n const values = record?.values\n const fields = useMemo(\n () => (record ? datasetRecordFields(model, values, resolveReference) : []),\n [record, model, values, resolveReference],\n )\n /**\n * The same mismatch report the editor shows, for the same reason: a type\n * change never rewrites stored documents, so a record may legitimately hold\n * a value its model no longer describes. Reporting it beside the value is\n * what stops the view claiming the record conforms.\n */\n const mismatches = useMemo(\n () => (record ? validateDocument(model, values ?? {}) : {}),\n [record, model, values],\n )\n\n return (\n <Dialog\n open={Boolean(record)}\n onClose={onClose}\n maxWidth=\"sm\"\n fullWidth\n aria-labelledby=\"dataset-record-view-title\"\n >\n <DialogTitle id=\"dataset-record-view-title\">{'View record'}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n <Box>\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {'Record ID'}\n </Typography>\n <Typography variant=\"body2\" sx={{ overflowWrap: 'anywhere' }}>\n {record?.$id ?? ''}\n </Typography>\n </Box>\n {fields.map((field) => (\n <Box key={field.fieldId}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap' }}\n >\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {field.label}\n </Typography>\n {field.source === 'extra' ? (\n <Tooltip\n title={\n 'Stored on this record with no field in the ' +\n 'collection’s schema. The table cannot show it, and ' +\n 'the next save through the editor removes it.'\n }\n >\n <Typography\n variant=\"caption\"\n color=\"warning.main\"\n sx={{ cursor: 'help' }}\n >\n {'not in schema'}\n </Typography>\n </Tooltip>\n ) : null}\n </Stack>\n <RecordFieldValue field={field} />\n {mismatches[field.fieldId] ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {mismatches[field.fieldId]}\n </Typography>\n ) : null}\n {field.description ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {field.description}\n </Typography>\n ) : null}\n </Box>\n ))}\n {fields.length === 0 ? (\n <Alert severity=\"info\">{'This record holds no fields.'}</Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n {onEdit ? <Button onClick={onEdit}>{'Edit record'}</Button> : null}\n <Button variant=\"contained\" color=\"primary\" onClick={onClose}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nDatasetRecordDialog.displayName = 'DatasetRecordDialog'\n\nexport default DatasetRecordDialog\n"],"names":["validateDocument","datasetRecordFields","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","Tooltip","Typography","useMemo","PLACEHOLDER","absent","null","RecordFieldValue","props","value","field","kind","variant","color","sx","fontStyle","text","references","direction","spacing","flexWrap","rowGap","map","reference","label","overflowWrap","id","items","component","m","pl","item","index","block","fontFamily","whiteSpace","displayName","DatasetRecordDialog","record","model","resolveReference","onClose","onEdit","values","fields","mismatches","open","Boolean","maxWidth","fullWidth","aria-labelledby","dividers","$id","alignItems","source","title","cursor","fieldId","description","length","severity","onClick"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/components/dataset-record-dialog.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type DatasetRecordField,\n type DatasetReferenceResolver,\n datasetRecordFields,\n} from '../model/dataset-record-view'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useMemo } from 'react'\nimport { type DatasetModel, validateDocument } from '../model/dataset-models'\n\n/**\n * Looking at a record, as its own gesture.\n *\n * The records table has room for the model's columns and one line per value,\n * so the only way to see what a record actually holds was to open it in the\n * EDITOR — which made reading a record and changing one the same click, on a\n * dialog whose primary button writes. This is the other half: the whole\n * record, nothing writable.\n *\n * ## It reads nothing\n *\n * Every value on screen arrives as a prop, out of the page the table already\n * listens to. There is no query, no `getDoc`, and no effect — opening a record\n * costs zero documents, and rendering the table does not open one dialog per\n * row to find out. That is not an optimization detail: a viewer that fetched\n * the record it was handed would bill a read per glance, and one that mounted\n * per row would bill the whole page on arrival.\n *\n * ## Read-only means read-only\n *\n * Nothing here takes input and nothing here writes. `Edit record` is a\n * separate, explicitly labelled action that closes this and opens the editor —\n * the dialog that has always owned writing — so the two gestures stay\n * distinct rather than one being a slip away from the other.\n */\nexport interface DatasetRecordDialogProps {\n /**\n * The record row itself, straight from the table's page. `null` closes the\n * dialog — including when the row leaves the page under it, which is the\n * honest response to a record this reader can no longer see.\n */\n record: { $id: string; values?: Record<string, unknown> } | null\n model: DatasetModel\n /** Reference target ID → label, or `null` when it resolves to nothing. */\n resolveReference?: DatasetReferenceResolver\n onClose: () => void\n /**\n * Opens the record in the editor. Omitted for a reader who cannot write, so\n * the control is absent rather than present-and-refusing.\n */\n onEdit?: () => void\n}\n\n/** What each \"no value\" state is called on screen. */\nconst PLACEHOLDER: Record<string, string> = {\n absent: 'Not set',\n null: 'Null',\n 'empty-text': 'Empty text',\n 'empty-list': 'Empty list',\n 'empty-map': 'Empty map',\n}\n\n/**\n * One field's value.\n *\n * The four empty states get four different words, deliberately. A table cell\n * prints `--` for all of them because it has one line to work with; here there\n * is room to say which, and the difference decides what a reader does next —\n * a field that was never written and a field holding a real empty string are\n * not the same finding.\n */\nfunction RecordFieldValue(props: { field: DatasetRecordField }) {\n const { value } = props.field\n if (value.kind === 'opaque') {\n return (\n <Typography\n variant=\"body2\"\n color=\"text.secondary\"\n sx={{ fontStyle: 'italic' }}\n >\n {value.text}\n </Typography>\n )\n }\n if (value.kind !== 'value') {\n return (\n <Typography\n variant=\"body2\"\n color=\"text.disabled\"\n sx={{ fontStyle: 'italic' }}\n >\n {PLACEHOLDER[value.kind] ?? 'Not set'}\n </Typography>\n )\n }\n if (value.references) {\n return (\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {value.references.map((reference) => (\n <Typography\n key={reference.id}\n variant=\"body2\"\n // An unresolved target is a fact about the data, not a rendering\n // failure to paper over: the ID is shown either way, and only the\n // resolved one is allowed to look like a working link.\n color={reference.label ? 'text.primary' : 'warning.main'}\n sx={{ overflowWrap: 'anywhere' }}\n >\n {reference.label ?? `${reference.id} · unresolved`}\n </Typography>\n ))}\n </Stack>\n )\n }\n if (value.items) {\n return (\n // One line per item. The comma-joined form a cell shows cannot say\n // whether `a, b` is one item or two; a list of lines can.\n <Stack component=\"ul\" spacing={0.25} sx={{ m: 0, pl: 2.5 }}>\n {value.items.map((item, index) => (\n <Typography\n // Items are values, not identities — two equal strings are two\n // real entries and the position is what tells them apart.\n key={`${index}:${item}`}\n component=\"li\"\n variant=\"body2\"\n sx={{ overflowWrap: 'anywhere' }}\n >\n {item}\n </Typography>\n ))}\n </Stack>\n )\n }\n return (\n <Typography\n variant=\"body2\"\n // Pretty-printed JSON is only readable where its whitespace survives.\n {...(value.block\n ? {\n component: 'pre',\n sx: {\n m: 0,\n fontFamily: 'monospace',\n whiteSpace: 'pre-wrap',\n overflowWrap: 'anywhere',\n },\n }\n : { sx: { overflowWrap: 'anywhere' } })}\n >\n {value.text}\n </Typography>\n )\n}\nRecordFieldValue.displayName = 'RecordFieldValue'\n\nexport function DatasetRecordDialog(props: DatasetRecordDialogProps) {\n const { record, model, resolveReference, onClose, onEdit } = props\n const values = record?.values\n const fields = useMemo(\n () => (record ? datasetRecordFields(model, values, resolveReference) : []),\n [record, model, values, resolveReference],\n )\n /**\n * The same mismatch report the editor shows, for the same reason: a type\n * change never rewrites stored documents, so a record may legitimately hold\n * a value its model no longer describes. Reporting it beside the value is\n * what stops the view claiming the record conforms.\n */\n const mismatches = useMemo(\n () => (record ? validateDocument(model, values ?? {}) : {}),\n [record, model, values],\n )\n\n return (\n <Dialog\n open={Boolean(record)}\n onClose={onClose}\n maxWidth=\"sm\"\n fullWidth\n aria-labelledby=\"dataset-record-view-title\"\n >\n <DialogTitle id=\"dataset-record-view-title\">{'View record'}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={2}>\n <Box>\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {'Record ID'}\n </Typography>\n <Typography variant=\"body2\" sx={{ overflowWrap: 'anywhere' }}>\n {record?.$id ?? ''}\n </Typography>\n </Box>\n {fields.map((field) => (\n <Box key={field.fieldId}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'baseline', flexWrap: 'wrap' }}\n >\n <Typography\n variant=\"overline\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {field.label}\n </Typography>\n {field.source === 'extra' ? (\n <Tooltip\n title={\n 'Stored on this record with no field in the ' +\n 'collection’s schema. The table cannot show it, and ' +\n 'the next save through the editor removes it.'\n }\n >\n <Typography\n variant=\"caption\"\n color=\"warning.main\"\n sx={{ cursor: 'help' }}\n >\n {'not in schema'}\n </Typography>\n </Tooltip>\n ) : null}\n </Stack>\n <RecordFieldValue field={field} />\n {mismatches[field.fieldId] ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {mismatches[field.fieldId]}\n </Typography>\n ) : null}\n {field.description ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n >\n {field.description}\n </Typography>\n ) : null}\n </Box>\n ))}\n {fields.length === 0 ? (\n <Alert severity=\"info\">{'This record holds no fields.'}</Alert>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n {onEdit ? <Button onClick={onEdit}>{'Edit record'}</Button> : null}\n <Button variant=\"contained\" color=\"primary\" onClick={onClose}>\n {'Close'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nDatasetRecordDialog.displayName = 'DatasetRecordDialog'\n\nexport default DatasetRecordDialog\n"],"names":["datasetRecordFields","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","Tooltip","Typography","useMemo","validateDocument","PLACEHOLDER","absent","null","RecordFieldValue","props","value","field","kind","variant","color","sx","fontStyle","text","references","direction","spacing","flexWrap","rowGap","map","reference","label","overflowWrap","id","items","component","m","pl","item","index","block","fontFamily","whiteSpace","displayName","DatasetRecordDialog","record","model","resolveReference","onClose","onEdit","values","fields","mismatches","open","Boolean","maxWidth","fullWidth","aria-labelledby","dividers","$id","alignItems","source","title","cursor","fieldId","description","length","severity","onClick"],"mappings":"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"}
@@ -14,10 +14,10 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type DatasetModel } from '@aglyn/aglyn';
18
17
  import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
19
18
  import type { ListFilterClause, ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
20
19
  import { type ListQueryDeclaration, type ListQueryNormalizers, type ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan';
20
+ import { type DatasetModel } from '../model/dataset-models';
21
21
  /**
22
22
  * The grid column a dataset field is shown in. Prefixed, because field ids
23
23
  * are the dataset author's and may be any name, `actions` included.
@@ -1,4 +1,3 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
1
  /**
3
2
  * @license
4
3
  * Copyright 2026 Aglyn LLC
@@ -14,8 +13,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
14
  * See the License for the specific language governing permissions and
16
15
  * limitations under the License.
17
- */ import { DATASET_FILTER_PREFIX_MAX, datasetFilterTextKey, datasetFilterToken, datasetFilterValuePath, datasetFilterWords, datasetSearchToken } from "@aglyn/aglyn";
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
18
17
  import { LIST_QUERY_ID_PATH, planListQuery } from "@aglyn/shared-ui-jsx/const/list-query-plan";
18
+ import { DATASET_FILTER_PREFIX_MAX, datasetFilterTextKey, datasetFilterToken, datasetFilterValuePath, datasetFilterWords, datasetSearchToken } from "../model/dataset-models.js";
19
19
  /*
20
20
  * THE RECORDS TABLE IS ONE QUERY (AGL-3321).
21
21
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/components/dataset-record-filter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DATASET_FILTER_PREFIX_MAX,\n type DatasetModel,\n datasetFilterTextKey,\n datasetFilterToken,\n datasetFilterValuePath,\n datasetFilterWords,\n datasetSearchToken,\n} from '@aglyn/aglyn'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListFilterClause,\n ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryNormalizers,\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE RECORDS TABLE IS ONE QUERY (AGL-3321).\n *\n * Every clause in the Filters panel and the quick-search word are\n * predicates on the records query, planned by the shared `planListQuery`\n * and answered by Firestore; nothing is matched over rows already loaded.\n *\n * A record's fields live under `values`, which is exempt from indexing (see\n * `datasetFilterKeys`), so the query reads the two fields every writer\n * stamps beside it:\n *\n * `filterValues.<fieldId> ==` an equality — a picked option, a\n * boolean, a number, plain text (by its\n * lower-cased key);\n * `filterKeys array-contains` ONE word-level clause — a text\n * `contains` (`f:<id>^<prefix>`), a list\n * member (`f:<id>=<member>`), or the\n * quick-search word (`s:<prefix>`).\n *\n * Firestore's automatic single-field indexes compose any number of those\n * equalities and one `array-contains` under `orderBy(documentId())` by index\n * merging, so the table needs NO composite index — which it could not have:\n * every dataset's fields are its author's, and a composite per field per\n * dataset is not something an index file can list.\n *\n * What one such query cannot hold is not offered, or refused by name:\n *\n * - no ranges. `>` on a number or a date on a timestamp is one inequality,\n * which must then lead the order, and beside any equality it needs a\n * composite per dataset. Numbers offer `=`, timestamps nothing.\n * - no negations and no empty checks: Firestore has neither for a field a\n * writer omits.\n * - one array clause: a `contains` beside the search, or a second\n * `contains`, is refused through `ListQueryNotices`, never half-applied.\n * - one word of a `contains` or a search: the first word is asked, and the\n * table says so for the rest.\n */\n\n/** The array the word-level clauses and the quick search ask. */\nconst TOKENS_PATH = 'filterKeys'\n\n/**\n * The grid column a dataset field is shown in. Prefixed, because field ids\n * are the dataset author's and may be any name, `actions` included.\n */\nexport const recordColumn = (fieldId: string) => `values.${fieldId}`\nconst COLUMN_PREFIX = recordColumn('')\nconst fieldIdOf = (column: string) =>\n column.startsWith(COLUMN_PREFIX) ? column.slice(COLUMN_PREFIX.length) : column\n\n/**\n * How the records' filter fields were normalized (`dataset-models.ts`): a\n * typed text value becomes its `filterValues` key, and a search word its\n * `s:` token — so the query asks for exactly what the writers stored.\n */\nexport const datasetRecordNormalizers: ListQueryNormalizers = {\n key: datasetFilterTextKey,\n token: (value) => datasetSearchToken(value) ?? '',\n // No field offers `endsWith`, so nothing is ever reversed.\n reversed: (value) => value,\n maxPrefix: DATASET_FILTER_PREFIX_MAX,\n}\n\n/** What the records grid's Filters panel offers for one dataset model. */\nexport interface DatasetRecordFilter {\n /** The query declaration: every field below, the id order, the search. */\n declaration: ListQueryDeclaration\n fields: ListFilterField[]\n options: Record<string, ListFilterOption[]>\n headers: Record<string, string>\n selectFields: string[]\n}\n\nconst BOOLEAN_OPTIONS: ListFilterOption[] = [\n { value: 'true', label: 'True' },\n { value: 'false', label: 'False' },\n]\n\n/**\n * The model's fields as query fields, each shown in column\n * `values.<fieldId>`:\n *\n * enum text picked: is, is any of `filterValues.<id>`\n * plain text contains (one word), equals, `filterKeys`,\n * is any of (ignoring case) `filterValues.<id>`\n * bool picked: is `filterValues.<id>`\n * numbers = `filterValues.<id>`\n * list contains (a whole member) `filterKeys`\n *\n * Timestamps, references, maps, bytes, coordinates and nil fields offer no\n * filter. A field whose id no query path can name (a v1 column with a `.`\n * in it — `datasetFilterValuePath`) offers only its `contains`.\n */\nexport function datasetRecordFilter(model: DatasetModel): DatasetRecordFilter {\n const fields: ListFilterField[] = []\n const options: Record<string, ListFilterOption[]> = {}\n const headers: Record<string, string> = {}\n const selectFields: string[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const column = recordColumn(fieldId)\n const valuePath = datasetFilterValuePath(fieldId)\n const choices = field.type === 'text' ? (field.validation?.options ?? []) : []\n let declared: ListFilterField | null = null\n if (field.type === 'text' && choices.length) {\n if (valuePath) {\n declared = { column, kind: 'exact', path: valuePath, operators: ['equals', 'isAnyOf'] }\n options[column] = choices.map((choice) => ({ value: choice, label: choice }))\n }\n } else if (field.type === 'text') {\n // A `contains` asks the token the clause's value is turned into\n // (`recordQueryClause`), matched as given.\n declared = {\n column,\n kind: 'text',\n path: valuePath ?? column,\n tokensPath: TOKENS_PATH,\n verbatimTokens: true,\n ...(valuePath ? { lowerPath: valuePath } : {}),\n operators: valuePath ? ['contains', 'equals', 'isAnyOf'] : ['contains'],\n }\n } else if (field.type === 'bool') {\n if (valuePath) {\n declared = { column, kind: 'boolean', path: valuePath, operators: ['equals'] }\n options[column] = BOOLEAN_OPTIONS\n }\n } else if (field.type === 'int32' || field.type === 'int64' || field.type === 'float') {\n if (valuePath) declared = { column, kind: 'number', path: valuePath, operators: ['='] }\n } else if (field.type === 'sorted') {\n declared = {\n column,\n kind: 'text',\n path: column,\n tokensPath: TOKENS_PATH,\n verbatimTokens: true,\n operators: ['contains'],\n }\n }\n if (!declared) continue\n fields.push(declared)\n headers[column] = field.name || fieldId\n if (options[column]) selectFields.push(column)\n }\n return {\n declaration: {\n fields,\n // The document name: the one order no record can be missing (see the\n // card), and the one every automatic index already ends in.\n sorts: [{ path: LIST_QUERY_ID_PATH, direction: 'asc' }],\n search: { tokensPath: TOKENS_PATH },\n },\n fields,\n options,\n headers,\n selectFields,\n }\n}\n\n/**\n * A clause as the query asks it: a `contains` becomes the `filterKeys`\n * token of its first word (text) or of its member (list). Everything else\n * goes as typed — the planner applies the text key, the number, the boolean.\n */\nfunction recordQueryClause(model: DatasetModel, clause: ListFilterClause): ListFilterRequest {\n if (clause.op !== 'contains') return { field: clause.field, op: clause.op, value: clause.value }\n const token = datasetFilterToken(model, {\n field: fieldIdOf(clause.field),\n op: 'contains',\n value: clause.value,\n })\n return { field: clause.field, op: clause.op, value: token ?? '' }\n}\n\nconst clipWord = (word: string) =>\n Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX).join('')\n\n/** The records query, and what it says to the reader. */\nexport interface DatasetRecordPlan {\n plan: ListQueryPlan\n /** What was not put on the query, as the reader asked it, and why. */\n refused: Array<{ clause: ListFilterClause | 'search'; reason: string }>\n /** What WAS served, where it differs from what was typed. */\n notices: string[]\n}\n\n/**\n * The clauses and search words as ONE records query (`planListQuery`), with\n * the refusals mapped back to the clauses as the reader typed them, and the\n * notices said in the reader's words rather than as stored tokens.\n */\nexport function planRecordQuery(\n model: DatasetModel,\n filter: DatasetRecordFilter,\n clauses: readonly ListFilterClause[],\n searchWords: readonly string[],\n): DatasetRecordPlan {\n const asked = new Map<ListFilterRequest, ListFilterClause>()\n const queryClauses = clauses.map((clause) => {\n const queryClause = recordQueryClause(model, clause)\n asked.set(queryClause, clause)\n return queryClause\n })\n const plan = planListQuery(\n filter.declaration,\n { clauses: queryClauses, search: searchWords },\n datasetRecordNormalizers,\n )\n const refused = plan.refused.map((entry) => ({\n clause: entry.clause === 'search' ? ('search' as const) : (asked.get(entry.clause) ?? entry.clause),\n reason: entry.reason,\n }))\n\n const notices: string[] = []\n const oneWord = (label: string, text: string) => {\n const words = datasetFilterWords(text)\n if (words.length > 1) {\n notices.push(\n `${label} matches one word at a time: showing records with a word starting \"${clipWord(words[0])}\".`,\n )\n }\n if (words.some((word) => Array.from(word).length > DATASET_FILTER_PREFIX_MAX)) {\n notices.push(`${label} reads the first ${DATASET_FILTER_PREFIX_MAX} letters of a word.`)\n }\n }\n if (plan.searched) oneWord('Search', searchWords.join(' '))\n for (const served of plan.served) {\n const clause = asked.get(served)\n // A list's `contains` is a whole member; only text is asked by word.\n if (clause?.op !== 'contains') continue\n if (model.fields?.[fieldIdOf(clause.field)]?.type !== 'text') continue\n oneWord(`${filter.headers[clause.field] ?? clause.field} contains`, clause.value)\n }\n return { plan, refused, notices }\n}\n"],"names":["DATASET_FILTER_PREFIX_MAX","datasetFilterTextKey","datasetFilterToken","datasetFilterValuePath","datasetFilterWords","datasetSearchToken","LIST_QUERY_ID_PATH","planListQuery","TOKENS_PATH","recordColumn","fieldId","COLUMN_PREFIX","fieldIdOf","column","startsWith","slice","length","datasetRecordNormalizers","key","token","value","reversed","maxPrefix","BOOLEAN_OPTIONS","label","datasetRecordFilter","model","fields","options","headers","selectFields","order","field","valuePath","choices","type","validation","declared","kind","path","operators","map","choice","tokensPath","verbatimTokens","lowerPath","push","name","declaration","sorts","direction","search","recordQueryClause","clause","op","clipWord","word","Array","from","join","planRecordQuery","filter","clauses","searchWords","asked","Map","queryClauses","queryClause","set","plan","refused","entry","get","reason","notices","oneWord","text","words","some","searched","served"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/components/dataset-record-filter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListFilterClause,\n ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryNormalizers,\n type ListQueryPlan,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { DATASET_FILTER_PREFIX_MAX, type DatasetModel, datasetFilterTextKey, datasetFilterToken, datasetFilterValuePath, datasetFilterWords, datasetSearchToken } from '../model/dataset-models'\n\n/*\n * THE RECORDS TABLE IS ONE QUERY (AGL-3321).\n *\n * Every clause in the Filters panel and the quick-search word are\n * predicates on the records query, planned by the shared `planListQuery`\n * and answered by Firestore; nothing is matched over rows already loaded.\n *\n * A record's fields live under `values`, which is exempt from indexing (see\n * `datasetFilterKeys`), so the query reads the two fields every writer\n * stamps beside it:\n *\n * `filterValues.<fieldId> ==` an equality — a picked option, a\n * boolean, a number, plain text (by its\n * lower-cased key);\n * `filterKeys array-contains` ONE word-level clause — a text\n * `contains` (`f:<id>^<prefix>`), a list\n * member (`f:<id>=<member>`), or the\n * quick-search word (`s:<prefix>`).\n *\n * Firestore's automatic single-field indexes compose any number of those\n * equalities and one `array-contains` under `orderBy(documentId())` by index\n * merging, so the table needs NO composite index — which it could not have:\n * every dataset's fields are its author's, and a composite per field per\n * dataset is not something an index file can list.\n *\n * What one such query cannot hold is not offered, or refused by name:\n *\n * - no ranges. `>` on a number or a date on a timestamp is one inequality,\n * which must then lead the order, and beside any equality it needs a\n * composite per dataset. Numbers offer `=`, timestamps nothing.\n * - no negations and no empty checks: Firestore has neither for a field a\n * writer omits.\n * - one array clause: a `contains` beside the search, or a second\n * `contains`, is refused through `ListQueryNotices`, never half-applied.\n * - one word of a `contains` or a search: the first word is asked, and the\n * table says so for the rest.\n */\n\n/** The array the word-level clauses and the quick search ask. */\nconst TOKENS_PATH = 'filterKeys'\n\n/**\n * The grid column a dataset field is shown in. Prefixed, because field ids\n * are the dataset author's and may be any name, `actions` included.\n */\nexport const recordColumn = (fieldId: string) => `values.${fieldId}`\nconst COLUMN_PREFIX = recordColumn('')\nconst fieldIdOf = (column: string) =>\n column.startsWith(COLUMN_PREFIX) ? column.slice(COLUMN_PREFIX.length) : column\n\n/**\n * How the records' filter fields were normalized (`dataset-models.ts`): a\n * typed text value becomes its `filterValues` key, and a search word its\n * `s:` token — so the query asks for exactly what the writers stored.\n */\nexport const datasetRecordNormalizers: ListQueryNormalizers = {\n key: datasetFilterTextKey,\n token: (value) => datasetSearchToken(value) ?? '',\n // No field offers `endsWith`, so nothing is ever reversed.\n reversed: (value) => value,\n maxPrefix: DATASET_FILTER_PREFIX_MAX,\n}\n\n/** What the records grid's Filters panel offers for one dataset model. */\nexport interface DatasetRecordFilter {\n /** The query declaration: every field below, the id order, the search. */\n declaration: ListQueryDeclaration\n fields: ListFilterField[]\n options: Record<string, ListFilterOption[]>\n headers: Record<string, string>\n selectFields: string[]\n}\n\nconst BOOLEAN_OPTIONS: ListFilterOption[] = [\n { value: 'true', label: 'True' },\n { value: 'false', label: 'False' },\n]\n\n/**\n * The model's fields as query fields, each shown in column\n * `values.<fieldId>`:\n *\n * enum text picked: is, is any of `filterValues.<id>`\n * plain text contains (one word), equals, `filterKeys`,\n * is any of (ignoring case) `filterValues.<id>`\n * bool picked: is `filterValues.<id>`\n * numbers = `filterValues.<id>`\n * list contains (a whole member) `filterKeys`\n *\n * Timestamps, references, maps, bytes, coordinates and nil fields offer no\n * filter. A field whose id no query path can name (a v1 column with a `.`\n * in it — `datasetFilterValuePath`) offers only its `contains`.\n */\nexport function datasetRecordFilter(model: DatasetModel): DatasetRecordFilter {\n const fields: ListFilterField[] = []\n const options: Record<string, ListFilterOption[]> = {}\n const headers: Record<string, string> = {}\n const selectFields: string[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const column = recordColumn(fieldId)\n const valuePath = datasetFilterValuePath(fieldId)\n const choices = field.type === 'text' ? (field.validation?.options ?? []) : []\n let declared: ListFilterField | null = null\n if (field.type === 'text' && choices.length) {\n if (valuePath) {\n declared = { column, kind: 'exact', path: valuePath, operators: ['equals', 'isAnyOf'] }\n options[column] = choices.map((choice) => ({ value: choice, label: choice }))\n }\n } else if (field.type === 'text') {\n // A `contains` asks the token the clause's value is turned into\n // (`recordQueryClause`), matched as given.\n declared = {\n column,\n kind: 'text',\n path: valuePath ?? column,\n tokensPath: TOKENS_PATH,\n verbatimTokens: true,\n ...(valuePath ? { lowerPath: valuePath } : {}),\n operators: valuePath ? ['contains', 'equals', 'isAnyOf'] : ['contains'],\n }\n } else if (field.type === 'bool') {\n if (valuePath) {\n declared = { column, kind: 'boolean', path: valuePath, operators: ['equals'] }\n options[column] = BOOLEAN_OPTIONS\n }\n } else if (field.type === 'int32' || field.type === 'int64' || field.type === 'float') {\n if (valuePath) declared = { column, kind: 'number', path: valuePath, operators: ['='] }\n } else if (field.type === 'sorted') {\n declared = {\n column,\n kind: 'text',\n path: column,\n tokensPath: TOKENS_PATH,\n verbatimTokens: true,\n operators: ['contains'],\n }\n }\n if (!declared) continue\n fields.push(declared)\n headers[column] = field.name || fieldId\n if (options[column]) selectFields.push(column)\n }\n return {\n declaration: {\n fields,\n // The document name: the one order no record can be missing (see the\n // card), and the one every automatic index already ends in.\n sorts: [{ path: LIST_QUERY_ID_PATH, direction: 'asc' }],\n search: { tokensPath: TOKENS_PATH },\n },\n fields,\n options,\n headers,\n selectFields,\n }\n}\n\n/**\n * A clause as the query asks it: a `contains` becomes the `filterKeys`\n * token of its first word (text) or of its member (list). Everything else\n * goes as typed — the planner applies the text key, the number, the boolean.\n */\nfunction recordQueryClause(model: DatasetModel, clause: ListFilterClause): ListFilterRequest {\n if (clause.op !== 'contains') return { field: clause.field, op: clause.op, value: clause.value }\n const token = datasetFilterToken(model, {\n field: fieldIdOf(clause.field),\n op: 'contains',\n value: clause.value,\n })\n return { field: clause.field, op: clause.op, value: token ?? '' }\n}\n\nconst clipWord = (word: string) =>\n Array.from(word).slice(0, DATASET_FILTER_PREFIX_MAX).join('')\n\n/** The records query, and what it says to the reader. */\nexport interface DatasetRecordPlan {\n plan: ListQueryPlan\n /** What was not put on the query, as the reader asked it, and why. */\n refused: Array<{ clause: ListFilterClause | 'search'; reason: string }>\n /** What WAS served, where it differs from what was typed. */\n notices: string[]\n}\n\n/**\n * The clauses and search words as ONE records query (`planListQuery`), with\n * the refusals mapped back to the clauses as the reader typed them, and the\n * notices said in the reader's words rather than as stored tokens.\n */\nexport function planRecordQuery(\n model: DatasetModel,\n filter: DatasetRecordFilter,\n clauses: readonly ListFilterClause[],\n searchWords: readonly string[],\n): DatasetRecordPlan {\n const asked = new Map<ListFilterRequest, ListFilterClause>()\n const queryClauses = clauses.map((clause) => {\n const queryClause = recordQueryClause(model, clause)\n asked.set(queryClause, clause)\n return queryClause\n })\n const plan = planListQuery(\n filter.declaration,\n { clauses: queryClauses, search: searchWords },\n datasetRecordNormalizers,\n )\n const refused = plan.refused.map((entry) => ({\n clause: entry.clause === 'search' ? ('search' as const) : (asked.get(entry.clause) ?? entry.clause),\n reason: entry.reason,\n }))\n\n const notices: string[] = []\n const oneWord = (label: string, text: string) => {\n const words = datasetFilterWords(text)\n if (words.length > 1) {\n notices.push(\n `${label} matches one word at a time: showing records with a word starting \"${clipWord(words[0])}\".`,\n )\n }\n if (words.some((word) => Array.from(word).length > DATASET_FILTER_PREFIX_MAX)) {\n notices.push(`${label} reads the first ${DATASET_FILTER_PREFIX_MAX} letters of a word.`)\n }\n }\n if (plan.searched) oneWord('Search', searchWords.join(' '))\n for (const served of plan.served) {\n const clause = asked.get(served)\n // A list's `contains` is a whole member; only text is asked by word.\n if (clause?.op !== 'contains') continue\n if (model.fields?.[fieldIdOf(clause.field)]?.type !== 'text') continue\n oneWord(`${filter.headers[clause.field] ?? clause.field} contains`, clause.value)\n }\n return { plan, refused, notices }\n}\n"],"names":["LIST_QUERY_ID_PATH","planListQuery","DATASET_FILTER_PREFIX_MAX","datasetFilterTextKey","datasetFilterToken","datasetFilterValuePath","datasetFilterWords","datasetSearchToken","TOKENS_PATH","recordColumn","fieldId","COLUMN_PREFIX","fieldIdOf","column","startsWith","slice","length","datasetRecordNormalizers","key","token","value","reversed","maxPrefix","BOOLEAN_OPTIONS","label","datasetRecordFilter","model","fields","options","headers","selectFields","order","field","valuePath","choices","type","validation","declared","kind","path","operators","map","choice","tokensPath","verbatimTokens","lowerPath","push","name","declaration","sorts","direction","search","recordQueryClause","clause","op","clipWord","word","Array","from","join","planRecordQuery","filter","clauses","searchWords","asked","Map","queryClauses","queryClause","set","plan","refused","entry","get","reason","notices","oneWord","text","words","some","searched","served"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type DatasetModel } from '@aglyn/aglyn';
1
+ import { type DatasetModel } from '../model/dataset-models';
2
2
  export interface DatasetSchemaDialogProps {
3
3
  /** Host context; resolves the owning org for the data scope. */
4
4
  hostId?: string;