@aglyn/plugins-data 1.0.0-beta.218 → 1.0.0-beta.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +11 -9
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +6 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +47 -0
- package/src/lib/declarations.console-server.js +74 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +43 -0
- package/src/lib/declarations.server.js +73 -0
- package/src/lib/declarations.server.js.map +1 -0
- package/src/lib/form-target/dataset-form-record-target.server.d.ts +40 -0
- package/src/lib/form-target/dataset-form-record-target.server.js +160 -0
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -0
- package/src/lib/form-target/form-dataset-binding-token.d.ts +35 -0
- package/src/lib/form-target/form-dataset-binding-token.js +107 -0
- package/src/lib/form-target/form-dataset-binding-token.js.map +1 -0
- package/src/lib/form-target/form-dataset-binding.d.ts +50 -0
- package/src/lib/form-target/form-dataset-binding.js +74 -0
- package/src/lib/form-target/form-dataset-binding.js.map +1 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.d.ts +39 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.js +65 -0
- package/src/lib/form-target/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +36 -0
- package/src/lib/model/dataset-csv.js +85 -0
- package/src/lib/model/dataset-csv.js.map +1 -0
- package/src/lib/model/dataset-io.d.ts +8 -7
- package/src/lib/model/dataset-io.js +9 -8
- package/src/lib/model/dataset-io.js.map +1 -1
- package/src/lib/model/dataset-record-list.d.ts +40 -0
- package/src/lib/model/dataset-record-list.js +61 -0
- package/src/lib/model/dataset-record-list.js.map +1 -0
- package/src/lib/model/dataset-record.d.ts +33 -0
- package/src/lib/model/dataset-record.js +68 -0
- package/src/lib/model/dataset-record.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -2
- package/src/lib/repeat/dataset-repeat-rows.js +2 -2
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +43 -0
- package/src/lib/repeat/dataset-repeat-rows.server.js +170 -0
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-source.js +3 -1
- package/src/lib/repeat/dataset-repeat-source.js.map +1 -1
- package/src/lib/server/announce-dataset-records.d.ts +37 -0
- package/src/lib/server/announce-dataset-records.js +54 -0
- package/src/lib/server/announce-dataset-records.js.map +1 -0
- package/src/lib/server/api-v1/datasets.d.ts +18 -0
- package/src/lib/server/api-v1/datasets.js +646 -0
- package/src/lib/server/api-v1/datasets.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +26 -0
- package/src/lib/server/api-v1/openapi.js +260 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +33 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/custom-field-types.d.ts +39 -0
- package/src/lib/server/custom-field-types.js +41 -0
- package/src/lib/server/custom-field-types.js.map +1 -0
- package/src/lib/server/dataset-figures.d.ts +61 -0
- package/src/lib/server/dataset-figures.js +413 -0
- package/src/lib/server/dataset-figures.js.map +1 -0
- package/src/lib/server/dataset-record-index.d.ts +18 -0
- package/src/lib/server/dataset-record-index.js +62 -0
- package/src/lib/server/dataset-record-index.js.map +1 -0
- package/src/lib/server/dataset-storage-meter.d.ts +29 -0
- package/src/lib/server/dataset-storage-meter.js +82 -0
- package/src/lib/server/dataset-storage-meter.js.map +1 -0
- package/src/lib/server/datasets-export-route.d.ts +57 -0
- package/src/lib/server/datasets-export-route.js +229 -0
- package/src/lib/server/datasets-export-route.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +38 -0
- package/src/lib/server/datasets-route.js +614 -0
- package/src/lib/server/datasets-route.js.map +1 -0
- package/src/lib/server.d.ts +23 -0
- package/src/lib/server.js +54 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.d.ts +87 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +184 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -0
- package/src/lib/usage-axes.d.ts +24 -0
- package/src/lib/usage-axes.js +32 -0
- package/src/lib/usage-axes.js.map +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { tokenSigningSecret } from "@aglyn/tenant-data-admin/server/media-signing";
|
|
18
|
+
import { safeEqual } from "@aglyn/tenant-data-admin/server/safe-equal";
|
|
19
|
+
import { createHmac } from "crypto";
|
|
20
|
+
/**
|
|
21
|
+
* A form's dataset binding, signed into the page that renders the form.
|
|
22
|
+
*
|
|
23
|
+
* `/api/forms/submit` is public and unauthenticated, so nothing in its body
|
|
24
|
+
* may choose which dataset a record lands in: a dataset id taken from the body
|
|
25
|
+
* let anyone add rows to any dataset the site can see. The binding is instead
|
|
26
|
+
* read off the form by the compose that renders the page, signed here, and
|
|
27
|
+
* carried by the form; the route writes only where a valid signature says.
|
|
28
|
+
*
|
|
29
|
+
* What a token grants is what the published form already does. Anyone can read
|
|
30
|
+
* one out of the page, and replaying it writes to that form's dataset with
|
|
31
|
+
* that form's field map, for the site it was minted on — nothing a visitor
|
|
32
|
+
* submitting the form could not already do.
|
|
33
|
+
*
|
|
34
|
+
* Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the
|
|
35
|
+
* other tenant tokens use, under its own `form-dataset-binding:` prefix so it
|
|
36
|
+
* can never be replayed as any other kind of token. No expiry: a published
|
|
37
|
+
* page can be served long after it rendered, and an expired token would drop
|
|
38
|
+
* a live form's records. Rotating the secret, or the version, retires every
|
|
39
|
+
* token at once.
|
|
40
|
+
*/ const VERSION = 'v1';
|
|
41
|
+
const PREFIX = 'form-dataset-binding';
|
|
42
|
+
/** Far above any real binding, so an oversized body is refused unparsed. */ const MAX_TOKEN_CHARS = 8192;
|
|
43
|
+
const signature = (payload)=>createHmac('sha256', tokenSigningSecret()).update(`${PREFIX}:${VERSION}:${payload}`).digest('hex');
|
|
44
|
+
/**
|
|
45
|
+
* The token a page carries for one form's binding on one site.
|
|
46
|
+
*
|
|
47
|
+
* Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this
|
|
48
|
+
* secret signs; a caller that renders a page decides what an unsigned form
|
|
49
|
+
* does.
|
|
50
|
+
*/ export function signFormDatasetBinding(hostId, binding) {
|
|
51
|
+
const body = _extends({
|
|
52
|
+
h: hostId
|
|
53
|
+
}, binding.datasetId ? {
|
|
54
|
+
d: binding.datasetId
|
|
55
|
+
} : {}, binding.datasetName ? {
|
|
56
|
+
n: binding.datasetName
|
|
57
|
+
} : {}, {
|
|
58
|
+
m: binding.fieldMap
|
|
59
|
+
});
|
|
60
|
+
const payload = Buffer.from(JSON.stringify(body)).toString('base64url');
|
|
61
|
+
return `${VERSION}.${payload}.${signature(payload)}`;
|
|
62
|
+
}
|
|
63
|
+
const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
64
|
+
/**
|
|
65
|
+
* The binding a token carries, or null when it is not a valid token for
|
|
66
|
+
* `hostId`.
|
|
67
|
+
*
|
|
68
|
+
* Null for anything else at all — not a string, oversized, another version, a
|
|
69
|
+
* signature that does not match, a site other than this one, or no signing
|
|
70
|
+
* secret — because the only safe reading of a binding that cannot be verified
|
|
71
|
+
* is that there is none.
|
|
72
|
+
*/ export function verifyFormDatasetBinding(hostId, token) {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null;
|
|
75
|
+
const [version, payload, signed, extra] = token.split('.');
|
|
76
|
+
if (version !== VERSION || !payload || !signed || extra !== undefined) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!safeEqual(signed, signature(payload))) return null;
|
|
80
|
+
const body = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
81
|
+
if (!hostId || (body == null ? void 0 : body.h) !== hostId) return null;
|
|
82
|
+
const datasetId = text(body.d, 128);
|
|
83
|
+
const datasetName = text(body.n, 60);
|
|
84
|
+
if (!datasetId && !datasetName) return null;
|
|
85
|
+
const fieldMap = {};
|
|
86
|
+
if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {
|
|
87
|
+
for (const [key, value] of Object.entries(body.m)){
|
|
88
|
+
const submittedKey = text(key, 64);
|
|
89
|
+
const datasetFieldId = text(value, 64);
|
|
90
|
+
if (submittedKey && datasetFieldId) {
|
|
91
|
+
fieldMap[submittedKey] = datasetFieldId;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return _extends({}, datasetId ? {
|
|
96
|
+
datasetId
|
|
97
|
+
} : {}, datasetName ? {
|
|
98
|
+
datasetName
|
|
99
|
+
} : {}, {
|
|
100
|
+
fieldMap
|
|
101
|
+
});
|
|
102
|
+
} catch (unused) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=form-dataset-binding-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/form-dataset-binding-token.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 { tokenSigningSecret } from '@aglyn/tenant-data-admin/server/media-signing'\nimport { safeEqual } from '@aglyn/tenant-data-admin/server/safe-equal'\nimport { createHmac } from 'crypto'\nimport type { FormDatasetBinding } from './form-dataset-binding'\n\n/**\n * A form's dataset binding, signed into the page that renders the form.\n *\n * `/api/forms/submit` is public and unauthenticated, so nothing in its body\n * may choose which dataset a record lands in: a dataset id taken from the body\n * let anyone add rows to any dataset the site can see. The binding is instead\n * read off the form by the compose that renders the page, signed here, and\n * carried by the form; the route writes only where a valid signature says.\n *\n * What a token grants is what the published form already does. Anyone can read\n * one out of the page, and replaying it writes to that form's dataset with\n * that form's field map, for the site it was minted on — nothing a visitor\n * submitting the form could not already do.\n *\n * Signed with the shared, fail-closed `TOKEN_SIGNING_SECRET`, the secret the\n * other tenant tokens use, under its own `form-dataset-binding:` prefix so it\n * can never be replayed as any other kind of token. No expiry: a published\n * page can be served long after it rendered, and an expired token would drop\n * a live form's records. Rotating the secret, or the version, retires every\n * token at once.\n */\n\nconst VERSION = 'v1'\nconst PREFIX = 'form-dataset-binding'\n/** Far above any real binding, so an oversized body is refused unparsed. */\nconst MAX_TOKEN_CHARS = 8192\n\ninterface SignedBinding {\n h: string\n d?: string\n n?: string\n m: Record<string, string>\n}\n\nconst signature = (payload: string): string =>\n createHmac('sha256', tokenSigningSecret())\n .update(`${PREFIX}:${VERSION}:${payload}`)\n .digest('hex')\n\n/**\n * The token a page carries for one form's binding on one site.\n *\n * Throws when `TOKEN_SIGNING_SECRET` is not configured, like every token this\n * secret signs; a caller that renders a page decides what an unsigned form\n * does.\n */\nexport function signFormDatasetBinding(\n hostId: string,\n binding: FormDatasetBinding,\n): string {\n const body: SignedBinding = {\n h: hostId,\n ...(binding.datasetId ? { d: binding.datasetId } : {}),\n ...(binding.datasetName ? { n: binding.datasetName } : {}),\n m: binding.fieldMap,\n }\n const payload = Buffer.from(JSON.stringify(body)).toString('base64url')\n return `${VERSION}.${payload}.${signature(payload)}`\n}\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The binding a token carries, or null when it is not a valid token for\n * `hostId`.\n *\n * Null for anything else at all — not a string, oversized, another version, a\n * signature that does not match, a site other than this one, or no signing\n * secret — because the only safe reading of a binding that cannot be verified\n * is that there is none.\n */\nexport function verifyFormDatasetBinding(\n hostId: string,\n token: unknown,\n): FormDatasetBinding | null {\n try {\n if (typeof token !== 'string' || token.length > MAX_TOKEN_CHARS) return null\n const [version, payload, signed, extra] = token.split('.')\n if (version !== VERSION || !payload || !signed || extra !== undefined) {\n return null\n }\n if (!safeEqual(signed, signature(payload))) return null\n const body = JSON.parse(\n Buffer.from(payload, 'base64url').toString('utf8'),\n ) as Partial<SignedBinding>\n if (!hostId || body?.h !== hostId) return null\n const datasetId = text(body.d, 128)\n const datasetName = text(body.n, 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n if (body.m && typeof body.m === 'object' && !Array.isArray(body.m)) {\n for (const [key, value] of Object.entries(body.m)) {\n const submittedKey = text(key, 64)\n const datasetFieldId = text(value, 64)\n if (submittedKey && datasetFieldId) {\n fieldMap[submittedKey] = datasetFieldId\n }\n }\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n } catch {\n return null\n }\n}\n"],"names":["tokenSigningSecret","safeEqual","createHmac","VERSION","PREFIX","MAX_TOKEN_CHARS","signature","payload","update","digest","signFormDatasetBinding","hostId","binding","body","h","datasetId","d","datasetName","n","m","fieldMap","Buffer","from","JSON","stringify","toString","text","value","max","trim","slice","verifyFormDatasetBinding","token","length","version","signed","extra","split","undefined","parse","Array","isArray","key","Object","entries","submittedKey","datasetFieldId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,gDAA+C;AAClF,SAASC,SAAS,QAAQ,6CAA4C;AACtE,SAASC,UAAU,QAAQ,SAAQ;AAGnC;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,MAAMC,UAAU;AAChB,MAAMC,SAAS;AACf,0EAA0E,GAC1E,MAAMC,kBAAkB;AASxB,MAAMC,YAAY,CAACC,UACjBL,WAAW,UAAUF,sBAClBQ,MAAM,CAAC,GAAGJ,OAAO,CAAC,EAAED,QAAQ,CAAC,EAAEI,SAAS,EACxCE,MAAM,CAAC;AAEZ;;;;;;CAMC,GACD,OAAO,SAASC,uBACdC,MAAc,EACdC,OAA2B;IAE3B,MAAMC,OAAsB;QAC1BC,GAAGH;OACCC,QAAQG,SAAS,GAAG;QAAEC,GAAGJ,QAAQG,SAAS;IAAC,IAAI,CAAC,GAChDH,QAAQK,WAAW,GAAG;QAAEC,GAAGN,QAAQK,WAAW;IAAC,IAAI,CAAC;QACxDE,GAAGP,QAAQQ,QAAQ;;IAErB,MAAMb,UAAUc,OAAOC,IAAI,CAACC,KAAKC,SAAS,CAACX,OAAOY,QAAQ,CAAC;IAC3D,OAAO,GAAGtB,QAAQ,CAAC,EAAEI,QAAQ,CAAC,EAAED,UAAUC,UAAU;AACtD;AAEA,MAAMmB,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;CAQC,GACD,OAAO,SAASG,yBACdpB,MAAc,EACdqB,KAAc;IAEd,IAAI;QACF,IAAI,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG5B,iBAAiB,OAAO;QACxE,MAAM,CAAC6B,SAAS3B,SAAS4B,QAAQC,MAAM,GAAGJ,MAAMK,KAAK,CAAC;QACtD,IAAIH,YAAY/B,WAAW,CAACI,WAAW,CAAC4B,UAAUC,UAAUE,WAAW;YACrE,OAAO;QACT;QACA,IAAI,CAACrC,UAAUkC,QAAQ7B,UAAUC,WAAW,OAAO;QACnD,MAAMM,OAAOU,KAAKgB,KAAK,CACrBlB,OAAOC,IAAI,CAACf,SAAS,aAAakB,QAAQ,CAAC;QAE7C,IAAI,CAACd,UAAUE,CAAAA,wBAAAA,KAAMC,CAAC,MAAKH,QAAQ,OAAO;QAC1C,MAAMI,YAAYW,KAAKb,KAAKG,CAAC,EAAE;QAC/B,MAAMC,cAAcS,KAAKb,KAAKK,CAAC,EAAE;QACjC,IAAI,CAACH,aAAa,CAACE,aAAa,OAAO;QACvC,MAAMG,WAAmC,CAAC;QAC1C,IAAIP,KAAKM,CAAC,IAAI,OAAON,KAAKM,CAAC,KAAK,YAAY,CAACqB,MAAMC,OAAO,CAAC5B,KAAKM,CAAC,GAAG;YAClE,KAAK,MAAM,CAACuB,KAAKf,MAAM,IAAIgB,OAAOC,OAAO,CAAC/B,KAAKM,CAAC,EAAG;gBACjD,MAAM0B,eAAenB,KAAKgB,KAAK;gBAC/B,MAAMI,iBAAiBpB,KAAKC,OAAO;gBACnC,IAAIkB,gBAAgBC,gBAAgB;oBAClC1B,QAAQ,CAACyB,aAAa,GAAGC;gBAC3B;YACF;QACF;QACA,OAAO,aACD/B,YAAY;YAAEA;QAAU,IAAI,CAAC,GAC7BE,cAAc;YAAEA;QAAY,IAAI,CAAC;YACrCG;;IAEJ,EAAE,eAAM;QACN,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import type { AglynNodeSchema, NodeId } from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* Where a form's submissions are also written as dataset records, read off
|
|
20
|
+
* the form as a page renders it.
|
|
21
|
+
*/
|
|
22
|
+
export interface FormDatasetBinding {
|
|
23
|
+
/** The dataset, by id, so renaming it never breaks the binding (AGL-556). */
|
|
24
|
+
datasetId?: string;
|
|
25
|
+
/** The legacy by-name binding, honored when no id is set (AGL-141). */
|
|
26
|
+
datasetName?: string;
|
|
27
|
+
/** Submitted field name → the dataset field id the value is stored under. */
|
|
28
|
+
fieldMap: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** The most fields a submission may carry, and so the most a map can name. */
|
|
31
|
+
export declare const FORM_DATASET_BINDING_MAX_FIELDS = 20;
|
|
32
|
+
/**
|
|
33
|
+
* The dataset binding a `form` node declares, or null when it declares none.
|
|
34
|
+
*
|
|
35
|
+
* The map is built the way the browser builds the submission: every
|
|
36
|
+
* `formField` under the form, in reading order, keyed by the name the field
|
|
37
|
+
* submits under (`fieldName`, or `field` when it has none), and a later field
|
|
38
|
+
* under the same name replaces an earlier one. A field with no
|
|
39
|
+
* `datasetFieldId` maps nothing and its value falls back to matching dataset
|
|
40
|
+
* fields by name.
|
|
41
|
+
*
|
|
42
|
+
* Read from the COMPOSED tree, so a form grafted from a layout, a reusable
|
|
43
|
+
* component or a form entity is read exactly as the page renders it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formDatasetBindingOf(nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null, formNodeId: NodeId): FormDatasetBinding | null;
|
|
46
|
+
/** Every `form` node in a composed tree that declares a dataset binding. */
|
|
47
|
+
export declare function formDatasetBindingsOf(nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null): Array<{
|
|
48
|
+
nodeId: NodeId;
|
|
49
|
+
binding: FormDatasetBinding;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { collectFormFieldNodeIds, FORM_COMPONENT_ID } from "@aglyn/aglyn/app-utils/forms";
|
|
18
|
+
/** The most fields a submission may carry, and so the most a map can name. */ export const FORM_DATASET_BINDING_MAX_FIELDS = 20;
|
|
19
|
+
const text = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
20
|
+
/**
|
|
21
|
+
* The dataset binding a `form` node declares, or null when it declares none.
|
|
22
|
+
*
|
|
23
|
+
* The map is built the way the browser builds the submission: every
|
|
24
|
+
* `formField` under the form, in reading order, keyed by the name the field
|
|
25
|
+
* submits under (`fieldName`, or `field` when it has none), and a later field
|
|
26
|
+
* under the same name replaces an earlier one. A field with no
|
|
27
|
+
* `datasetFieldId` maps nothing and its value falls back to matching dataset
|
|
28
|
+
* fields by name.
|
|
29
|
+
*
|
|
30
|
+
* Read from the COMPOSED tree, so a form grafted from a layout, a reusable
|
|
31
|
+
* component or a form entity is read exactly as the page renders it.
|
|
32
|
+
*/ export function formDatasetBindingOf(nodes, formNodeId) {
|
|
33
|
+
var _node_props;
|
|
34
|
+
const node = nodes == null ? void 0 : nodes[formNodeId];
|
|
35
|
+
if ((node == null ? void 0 : node.componentId) !== FORM_COMPONENT_ID) return null;
|
|
36
|
+
const props = (_node_props = node.props) != null ? _node_props : {};
|
|
37
|
+
const datasetId = text(props['datasetId'], 128);
|
|
38
|
+
const datasetName = text(props['datasetName'], 60);
|
|
39
|
+
if (!datasetId && !datasetName) return null;
|
|
40
|
+
const fieldMap = {};
|
|
41
|
+
for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)){
|
|
42
|
+
var _ref;
|
|
43
|
+
var _nodes_fieldNodeId;
|
|
44
|
+
const fieldProps = (_ref = nodes == null ? void 0 : (_nodes_fieldNodeId = nodes[fieldNodeId]) == null ? void 0 : _nodes_fieldNodeId.props) != null ? _ref : {};
|
|
45
|
+
const datasetFieldId = text(fieldProps['datasetFieldId'], 64);
|
|
46
|
+
if (!datasetFieldId) continue;
|
|
47
|
+
const submittedKey = text(fieldProps['fieldName'], 64) || 'field';
|
|
48
|
+
if (!(submittedKey in fieldMap) && Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
fieldMap[submittedKey] = datasetFieldId;
|
|
52
|
+
}
|
|
53
|
+
return _extends({}, datasetId ? {
|
|
54
|
+
datasetId
|
|
55
|
+
} : {}, datasetName ? {
|
|
56
|
+
datasetName
|
|
57
|
+
} : {}, {
|
|
58
|
+
fieldMap
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Every `form` node in a composed tree that declares a dataset binding. */ export function formDatasetBindingsOf(nodes) {
|
|
62
|
+
const found = [];
|
|
63
|
+
for (const [nodeId, node] of Object.entries(nodes != null ? nodes : {})){
|
|
64
|
+
if ((node == null ? void 0 : node.componentId) !== FORM_COMPONENT_ID) continue;
|
|
65
|
+
const binding = formDatasetBindingOf(nodes, nodeId);
|
|
66
|
+
if (binding) found.push({
|
|
67
|
+
nodeId: nodeId,
|
|
68
|
+
binding
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=form-dataset-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/form-dataset-binding.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 { AglynNodeSchema, NodeId } from '@aglyn/aglyn'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n} from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * Where a form's submissions are also written as dataset records, read off\n * the form as a page renders it.\n */\nexport interface FormDatasetBinding {\n /** The dataset, by id, so renaming it never breaks the binding (AGL-556). */\n datasetId?: string\n /** The legacy by-name binding, honored when no id is set (AGL-141). */\n datasetName?: string\n /** Submitted field name → the dataset field id the value is stored under. */\n fieldMap: Record<string, string>\n}\n\n/** The most fields a submission may carry, and so the most a map can name. */\nexport const FORM_DATASET_BINDING_MAX_FIELDS = 20\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * The dataset binding a `form` node declares, or null when it declares none.\n *\n * The map is built the way the browser builds the submission: every\n * `formField` under the form, in reading order, keyed by the name the field\n * submits under (`fieldName`, or `field` when it has none), and a later field\n * under the same name replaces an earlier one. A field with no\n * `datasetFieldId` maps nothing and its value falls back to matching dataset\n * fields by name.\n *\n * Read from the COMPOSED tree, so a form grafted from a layout, a reusable\n * component or a form entity is read exactly as the page renders it.\n */\nexport function formDatasetBindingOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n formNodeId: NodeId,\n): FormDatasetBinding | null {\n const node = nodes?.[formNodeId]\n if (node?.componentId !== FORM_COMPONENT_ID) return null\n const props = (node.props ?? {}) as Record<string, unknown>\n const datasetId = text(props['datasetId'], 128)\n const datasetName = text(props['datasetName'], 60)\n if (!datasetId && !datasetName) return null\n const fieldMap: Record<string, string> = {}\n for (const fieldNodeId of collectFormFieldNodeIds(nodes, formNodeId)) {\n const fieldProps = (nodes?.[fieldNodeId]?.props ?? {}) as Record<\n string,\n unknown\n >\n const datasetFieldId = text(fieldProps['datasetFieldId'], 64)\n if (!datasetFieldId) continue\n const submittedKey = text(fieldProps['fieldName'], 64) || 'field'\n if (\n !(submittedKey in fieldMap) &&\n Object.keys(fieldMap).length >= FORM_DATASET_BINDING_MAX_FIELDS\n ) {\n continue\n }\n fieldMap[submittedKey] = datasetFieldId\n }\n return {\n ...(datasetId ? { datasetId } : {}),\n ...(datasetName ? { datasetName } : {}),\n fieldMap,\n }\n}\n\n/** Every `form` node in a composed tree that declares a dataset binding. */\nexport function formDatasetBindingsOf(\n nodes: Record<NodeId, AglynNodeSchema | undefined> | undefined | null,\n): Array<{ nodeId: NodeId; binding: FormDatasetBinding }> {\n const found: Array<{ nodeId: NodeId; binding: FormDatasetBinding }> = []\n for (const [nodeId, node] of Object.entries(nodes ?? {})) {\n if (node?.componentId !== FORM_COMPONENT_ID) continue\n const binding = formDatasetBindingOf(nodes, nodeId as NodeId)\n if (binding) found.push({ nodeId: nodeId as NodeId, binding })\n }\n return found\n}\n"],"names":["collectFormFieldNodeIds","FORM_COMPONENT_ID","FORM_DATASET_BINDING_MAX_FIELDS","text","value","max","trim","slice","formDatasetBindingOf","nodes","formNodeId","node","componentId","props","datasetId","datasetName","fieldMap","fieldNodeId","fieldProps","datasetFieldId","submittedKey","Object","keys","length","formDatasetBindingsOf","found","nodeId","entries","binding","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,uBAAuB,EACvBC,iBAAiB,QACZ,+BAA8B;AAerC,4EAA4E,GAC5E,OAAO,MAAMC,kCAAkC,GAAE;AAEjD,MAAMC,OAAO,CAACC,OAAgBC,MAC5B,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,KAAK,CAAC,GAAGF,OAAO;AAE3D;;;;;;;;;;;;CAYC,GACD,OAAO,SAASG,qBACdC,KAAqE,EACrEC,UAAkB;QAIHC;IAFf,MAAMA,OAAOF,yBAAAA,KAAO,CAACC,WAAW;IAChC,IAAIC,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB,OAAO;IACpD,MAAMY,SAASF,cAAAA,KAAKE,KAAK,YAAVF,cAAc,CAAC;IAC9B,MAAMG,YAAYX,KAAKU,KAAK,CAAC,YAAY,EAAE;IAC3C,MAAME,cAAcZ,KAAKU,KAAK,CAAC,cAAc,EAAE;IAC/C,IAAI,CAACC,aAAa,CAACC,aAAa,OAAO;IACvC,MAAMC,WAAmC,CAAC;IAC1C,KAAK,MAAMC,eAAejB,wBAAwBS,OAAOC,YAAa;;YAChDD;QAApB,MAAMS,qBAAcT,0BAAAA,qBAAAA,KAAO,CAACQ,YAAY,qBAApBR,mBAAsBI,KAAK,mBAAI,CAAC;QAIpD,MAAMM,iBAAiBhB,KAAKe,UAAU,CAAC,iBAAiB,EAAE;QAC1D,IAAI,CAACC,gBAAgB;QACrB,MAAMC,eAAejB,KAAKe,UAAU,CAAC,YAAY,EAAE,OAAO;QAC1D,IACE,CAAEE,CAAAA,gBAAgBJ,QAAO,KACzBK,OAAOC,IAAI,CAACN,UAAUO,MAAM,IAAIrB,iCAChC;YACA;QACF;QACAc,QAAQ,CAACI,aAAa,GAAGD;IAC3B;IACA,OAAO,aACDL,YAAY;QAAEA;IAAU,IAAI,CAAC,GAC7BC,cAAc;QAAEA;IAAY,IAAI,CAAC;QACrCC;;AAEJ;AAEA,0EAA0E,GAC1E,OAAO,SAASQ,sBACdf,KAAqE;IAErE,MAAMgB,QAAgE,EAAE;IACxE,KAAK,MAAM,CAACC,QAAQf,KAAK,IAAIU,OAAOM,OAAO,CAAClB,gBAAAA,QAAS,CAAC,GAAI;QACxD,IAAIE,CAAAA,wBAAAA,KAAMC,WAAW,MAAKX,mBAAmB;QAC7C,MAAM2B,UAAUpB,qBAAqBC,OAAOiB;QAC5C,IAAIE,SAASH,MAAMI,IAAI,CAAC;YAAEH,QAAQA;YAAkBE;QAAQ;IAC9D;IACA,OAAOH;AACT"}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
* The prop a rendered `form` node carries its signed dataset binding in. The
|
|
19
|
+
* form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are
|
|
20
|
+
* on pages already rendered and cached — never rename.
|
|
21
|
+
*/
|
|
22
|
+
export declare const FORM_DATASET_BINDING_PROP = "datasetBindingToken";
|
|
23
|
+
/** The submission body field the form element posts the signed binding in. */
|
|
24
|
+
export declare const FORM_DATASET_BINDING_FIELD = "datasetBinding";
|
|
25
|
+
/**
|
|
26
|
+
* Signs every `form` node's dataset binding into its props, for this plugin's
|
|
27
|
+
* form record target to verify when the submission comes back (AGL-2773) —
|
|
28
|
+
* the stamping half of `plugin-manager/submission-record-target.ts`.
|
|
29
|
+
*
|
|
30
|
+
* A record is written only where a valid signature says, so a form
|
|
31
|
+
* without one still submits — the Inbox copy is always written — but adds no
|
|
32
|
+
* record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the
|
|
33
|
+
* tree is returned unsigned rather than failing the page render, and the
|
|
34
|
+
* missing secret is reported once per process instead of once per render.
|
|
35
|
+
*
|
|
36
|
+
* Never mutates `nodes`; a tree with no bound form comes back as the same
|
|
37
|
+
* object.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stampFormDatasetBindings<N extends Record<string, any>>(nodes: N, hostId: string): N;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { formDatasetBindingsOf } from "./form-dataset-binding.js";
|
|
18
|
+
import { signFormDatasetBinding } from "./form-dataset-binding-token.js";
|
|
19
|
+
/**
|
|
20
|
+
* The prop a rendered `form` node carries its signed dataset binding in. The
|
|
21
|
+
* form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are
|
|
22
|
+
* on pages already rendered and cached — never rename.
|
|
23
|
+
*/ export const FORM_DATASET_BINDING_PROP = 'datasetBindingToken';
|
|
24
|
+
/** The submission body field the form element posts the signed binding in. */ export const FORM_DATASET_BINDING_FIELD = 'datasetBinding';
|
|
25
|
+
/** Whether an unsigned render has been reported in this process yet. */ let reportedUnsigned = false;
|
|
26
|
+
/**
|
|
27
|
+
* Signs every `form` node's dataset binding into its props, for this plugin's
|
|
28
|
+
* form record target to verify when the submission comes back (AGL-2773) —
|
|
29
|
+
* the stamping half of `plugin-manager/submission-record-target.ts`.
|
|
30
|
+
*
|
|
31
|
+
* A record is written only where a valid signature says, so a form
|
|
32
|
+
* without one still submits — the Inbox copy is always written — but adds no
|
|
33
|
+
* record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the
|
|
34
|
+
* tree is returned unsigned rather than failing the page render, and the
|
|
35
|
+
* missing secret is reported once per process instead of once per render.
|
|
36
|
+
*
|
|
37
|
+
* Never mutates `nodes`; a tree with no bound form comes back as the same
|
|
38
|
+
* object.
|
|
39
|
+
*/ export function stampFormDatasetBindings(nodes, hostId) {
|
|
40
|
+
const bindings = formDatasetBindingsOf(nodes);
|
|
41
|
+
if (!bindings.length) return nodes;
|
|
42
|
+
const signed = _extends({}, nodes);
|
|
43
|
+
for (const { nodeId, binding } of bindings){
|
|
44
|
+
var _ref;
|
|
45
|
+
let token;
|
|
46
|
+
try {
|
|
47
|
+
token = signFormDatasetBinding(hostId, binding);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!reportedUnsigned) {
|
|
50
|
+
reportedUnsigned = true;
|
|
51
|
+
console.error('Form dataset bindings are not being signed, so forms bound to a ' + 'dataset will not write records', error);
|
|
52
|
+
}
|
|
53
|
+
return nodes;
|
|
54
|
+
}
|
|
55
|
+
const node = signed[nodeId];
|
|
56
|
+
signed[nodeId] = _extends({}, node, {
|
|
57
|
+
props: _extends({}, (_ref = node == null ? void 0 : node.props) != null ? _ref : {}, {
|
|
58
|
+
[FORM_DATASET_BINDING_PROP]: token
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return signed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=stamp-form-dataset-bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/form-target/stamp-form-dataset-bindings.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 { formDatasetBindingsOf } from './form-dataset-binding'\nimport { signFormDatasetBinding } from './form-dataset-binding-token'\n\n/**\n * The prop a rendered `form` node carries its signed dataset binding in. The\n * form element posts it back as {@link FORM_DATASET_BINDING_FIELD}. Both are\n * on pages already rendered and cached — never rename.\n */\nexport const FORM_DATASET_BINDING_PROP = 'datasetBindingToken'\n\n/** The submission body field the form element posts the signed binding in. */\nexport const FORM_DATASET_BINDING_FIELD = 'datasetBinding'\n\n/** Whether an unsigned render has been reported in this process yet. */\nlet reportedUnsigned = false\n\n/**\n * Signs every `form` node's dataset binding into its props, for this plugin's\n * form record target to verify when the submission comes back (AGL-2773) —\n * the stamping half of `plugin-manager/submission-record-target.ts`.\n *\n * A record is written only where a valid signature says, so a form\n * without one still submits — the Inbox copy is always written — but adds no\n * record. That is what happens when `TOKEN_SIGNING_SECRET` is missing: the\n * tree is returned unsigned rather than failing the page render, and the\n * missing secret is reported once per process instead of once per render.\n *\n * Never mutates `nodes`; a tree with no bound form comes back as the same\n * object.\n */\nexport function stampFormDatasetBindings<N extends Record<string, any>>(\n nodes: N,\n hostId: string,\n): N {\n const bindings = formDatasetBindingsOf(nodes as never)\n if (!bindings.length) return nodes\n const signed: Record<string, any> = { ...nodes }\n for (const { nodeId, binding } of bindings) {\n let token: string\n try {\n token = signFormDatasetBinding(hostId, binding)\n } catch (error) {\n if (!reportedUnsigned) {\n reportedUnsigned = true\n console.error(\n 'Form dataset bindings are not being signed, so forms bound to a ' +\n 'dataset will not write records',\n error,\n )\n }\n return nodes\n }\n const node = signed[nodeId]\n signed[nodeId] = {\n ...node,\n props: { ...(node?.props ?? {}), [FORM_DATASET_BINDING_PROP]: token },\n }\n }\n return signed as N\n}\n"],"names":["formDatasetBindingsOf","signFormDatasetBinding","FORM_DATASET_BINDING_PROP","FORM_DATASET_BINDING_FIELD","reportedUnsigned","stampFormDatasetBindings","nodes","hostId","bindings","length","signed","nodeId","binding","token","error","console","node","props"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,4BAAwB;AAC9D,SAASC,sBAAsB,QAAQ,kCAA8B;AAErE;;;;CAIC,GACD,OAAO,MAAMC,4BAA4B,sBAAqB;AAE9D,4EAA4E,GAC5E,OAAO,MAAMC,6BAA6B,iBAAgB;AAE1D,sEAAsE,GACtE,IAAIC,mBAAmB;AAEvB;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,yBACdC,KAAQ,EACRC,MAAc;IAEd,MAAMC,WAAWR,sBAAsBM;IACvC,IAAI,CAACE,SAASC,MAAM,EAAE,OAAOH;IAC7B,MAAMI,SAA8B,aAAKJ;IACzC,KAAK,MAAM,EAAEK,MAAM,EAAEC,OAAO,EAAE,IAAIJ,SAAU;;QAC1C,IAAIK;QACJ,IAAI;YACFA,QAAQZ,uBAAuBM,QAAQK;QACzC,EAAE,OAAOE,OAAO;YACd,IAAI,CAACV,kBAAkB;gBACrBA,mBAAmB;gBACnBW,QAAQD,KAAK,CACX,qEACE,kCACFA;YAEJ;YACA,OAAOR;QACT;QACA,MAAMU,OAAON,MAAM,CAACC,OAAO;QAC3BD,MAAM,CAACC,OAAO,GAAG,aACZK;YACHC,OAAO,qBAAMD,wBAAAA,KAAMC,KAAK,mBAAI,CAAC;gBAAI,CAACf,0BAA0B,EAAEW;;;IAElE;IACA,OAAOH;AACT"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { DatasetFieldDefinition, DatasetModel } from '@aglyn/aglyn/app-utils/dataset-models';
|
|
18
|
+
/** Storage value → portable string (CSV cell / JSON value). */
|
|
19
|
+
export declare function serializeDatasetValue(field: DatasetFieldDefinition, value: unknown): string;
|
|
20
|
+
/**
|
|
21
|
+
* The header line — field ids, in model order (AGL-2335).
|
|
22
|
+
*
|
|
23
|
+
* Split out of {@link datasetRecordsToCsv} so a server export can emit the
|
|
24
|
+
* file a page at a time. The whole point of that export is that it is not
|
|
25
|
+
* bounded by what one process can hold in memory, and a helper that only
|
|
26
|
+
* takes a complete `rows` array cannot serve it. Everything below shares
|
|
27
|
+
* one escaper: there are already five divergent hand-rolled CSV escapers in
|
|
28
|
+
* this repo and this is not becoming the sixth.
|
|
29
|
+
*/
|
|
30
|
+
export declare function datasetCsvHeader(model: DatasetModel): string;
|
|
31
|
+
/** One record's values (storage form) → one CSV line, no terminator. */
|
|
32
|
+
export declare function datasetCsvRow(model: DatasetModel, row: Record<string, unknown>): string;
|
|
33
|
+
/** One record's values → the portable JSON object the JSON export emits. */
|
|
34
|
+
export declare function datasetRecordToJson(model: DatasetModel, row: Record<string, unknown>): Record<string, string>;
|
|
35
|
+
/** Rows (storage-form value maps) → CSV with a fieldId header row. */
|
|
36
|
+
export declare function datasetRecordsToCsv(model: DatasetModel, rows: Array<Record<string, unknown>>): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* Dataset CSV/JSON serialization (AGL-182). The predictable inverse of
|
|
18
|
+
* `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined
|
|
19
|
+
* lists, JSON maps, reference ids — so an export re-imports losslessly
|
|
20
|
+
* through the same coercion.
|
|
21
|
+
*
|
|
22
|
+
* This plugin's own: the export route that streams a dataset a page at a
|
|
23
|
+
* time and the hub's client-side download both write through it. The
|
|
24
|
+
* escaper, the parser and the row-count check are the platform's
|
|
25
|
+
* (`app-utils/csv`), because the email, commerce and CRM imports read files
|
|
26
|
+
* too and may not reach this plugin.
|
|
27
|
+
*/ import { escapeCsvCell as csvEscape } from "@aglyn/aglyn/app-utils/csv";
|
|
28
|
+
/** Storage value → portable string (CSV cell / JSON value). */ export function serializeDatasetValue(field, value) {
|
|
29
|
+
if (value == null) return '';
|
|
30
|
+
switch(field.type){
|
|
31
|
+
case 'timestamp':
|
|
32
|
+
return typeof value === 'number' && Number.isFinite(value) ? new Date(value).toISOString() : String(value);
|
|
33
|
+
case 'coordinates':
|
|
34
|
+
{
|
|
35
|
+
const coordinates = value;
|
|
36
|
+
return coordinates && typeof coordinates.latitude === 'number' && typeof coordinates.longitude === 'number' ? `${coordinates.latitude}, ${coordinates.longitude}` : String(value);
|
|
37
|
+
}
|
|
38
|
+
case 'sorted':
|
|
39
|
+
case 'reference':
|
|
40
|
+
return Array.isArray(value) ? value.join(', ') : String(value);
|
|
41
|
+
case 'map':
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
} catch (unused) {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The header line — field ids, in model order (AGL-2335).
|
|
53
|
+
*
|
|
54
|
+
* Split out of {@link datasetRecordsToCsv} so a server export can emit the
|
|
55
|
+
* file a page at a time. The whole point of that export is that it is not
|
|
56
|
+
* bounded by what one process can hold in memory, and a helper that only
|
|
57
|
+
* takes a complete `rows` array cannot serve it. Everything below shares
|
|
58
|
+
* one escaper: there are already five divergent hand-rolled CSV escapers in
|
|
59
|
+
* this repo and this is not becoming the sixth.
|
|
60
|
+
*/ export function datasetCsvHeader(model) {
|
|
61
|
+
return model.order.map(csvEscape).join(',');
|
|
62
|
+
}
|
|
63
|
+
/** One record's values (storage form) → one CSV line, no terminator. */ export function datasetCsvRow(model, row) {
|
|
64
|
+
return model.order.map((fieldId)=>{
|
|
65
|
+
const field = model.fields[fieldId];
|
|
66
|
+
return csvEscape(field ? serializeDatasetValue(field, row[fieldId]) : '');
|
|
67
|
+
}).join(',');
|
|
68
|
+
}
|
|
69
|
+
/** One record's values → the portable JSON object the JSON export emits. */ export function datasetRecordToJson(model, row) {
|
|
70
|
+
return Object.fromEntries(model.order.map((fieldId)=>{
|
|
71
|
+
const field = model.fields[fieldId];
|
|
72
|
+
return [
|
|
73
|
+
fieldId,
|
|
74
|
+
field ? serializeDatasetValue(field, row[fieldId]) : ''
|
|
75
|
+
];
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/** Rows (storage-form value maps) → CSV with a fieldId header row. */ export function datasetRecordsToCsv(model, rows) {
|
|
79
|
+
return [
|
|
80
|
+
datasetCsvHeader(model),
|
|
81
|
+
...rows.map((row)=>datasetCsvRow(model, row))
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=dataset-csv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-csv.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\n/**\n * Dataset CSV/JSON serialization (AGL-182). The predictable inverse of\n * `coerceDocumentValues`: ISO dates, `lat, lon` coordinates, comma-joined\n * lists, JSON maps, reference ids — so an export re-imports losslessly\n * through the same coercion.\n *\n * This plugin's own: the export route that streams a dataset a page at a\n * time and the hub's client-side download both write through it. The\n * escaper, the parser and the row-count check are the platform's\n * (`app-utils/csv`), because the email, commerce and CRM imports read files\n * too and may not reach this plugin.\n */\n\nimport { escapeCsvCell as csvEscape } from '@aglyn/aglyn/app-utils/csv'\nimport type {\n DatasetFieldDefinition,\n DatasetModel,\n} from '@aglyn/aglyn/app-utils/dataset-models'\n\n\n/** Storage value → portable string (CSV cell / JSON value). */\nexport function serializeDatasetValue(\n field: DatasetFieldDefinition,\n value: unknown,\n): string {\n if (value == null) return ''\n switch (field.type) {\n case 'timestamp':\n return typeof value === 'number' && Number.isFinite(value)\n ? new Date(value).toISOString()\n : String(value)\n case 'coordinates': {\n const coordinates = value as { latitude?: number; longitude?: number }\n return coordinates &&\n typeof coordinates.latitude === 'number' &&\n typeof coordinates.longitude === 'number'\n ? `${coordinates.latitude}, ${coordinates.longitude}`\n : String(value)\n }\n case 'sorted':\n case 'reference':\n return Array.isArray(value) ? value.join(', ') : String(value)\n case 'map':\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n default:\n return String(value)\n }\n}\n\n/**\n * The header line — field ids, in model order (AGL-2335).\n *\n * Split out of {@link datasetRecordsToCsv} so a server export can emit the\n * file a page at a time. The whole point of that export is that it is not\n * bounded by what one process can hold in memory, and a helper that only\n * takes a complete `rows` array cannot serve it. Everything below shares\n * one escaper: there are already five divergent hand-rolled CSV escapers in\n * this repo and this is not becoming the sixth.\n */\nexport function datasetCsvHeader(model: DatasetModel): string {\n return model.order.map(csvEscape).join(',')\n}\n\n/** One record's values (storage form) → one CSV line, no terminator. */\nexport function datasetCsvRow(\n model: DatasetModel,\n row: Record<string, unknown>,\n): string {\n return model.order\n .map((fieldId) => {\n const field = model.fields[fieldId]\n return csvEscape(field ? serializeDatasetValue(field, row[fieldId]) : '')\n })\n .join(',')\n}\n\n/** One record's values → the portable JSON object the JSON export emits. */\nexport function datasetRecordToJson(\n model: DatasetModel,\n row: Record<string, unknown>,\n): Record<string, string> {\n return Object.fromEntries(\n model.order.map((fieldId) => {\n const field = model.fields[fieldId]\n return [fieldId, field ? serializeDatasetValue(field, row[fieldId]) : '']\n }),\n )\n}\n\n/** Rows (storage-form value maps) → CSV with a fieldId header row. */\nexport function datasetRecordsToCsv(\n model: DatasetModel,\n rows: Array<Record<string, unknown>>,\n): string {\n return [\n datasetCsvHeader(model),\n ...rows.map((row) => datasetCsvRow(model, row)),\n ].join('\\n')\n}\n"],"names":["escapeCsvCell","csvEscape","serializeDatasetValue","field","value","type","Number","isFinite","Date","toISOString","String","coordinates","latitude","longitude","Array","isArray","join","JSON","stringify","datasetCsvHeader","model","order","map","datasetCsvRow","row","fieldId","fields","datasetRecordToJson","Object","fromEntries","datasetRecordsToCsv","rows"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,SAASA,iBAAiBC,SAAS,QAAQ,6BAA4B;AAOvE,6DAA6D,GAC7D,OAAO,SAASC,sBACdC,KAA6B,EAC7BC,KAAc;IAEd,IAAIA,SAAS,MAAM,OAAO;IAC1B,OAAQD,MAAME,IAAI;QAChB,KAAK;YACH,OAAO,OAAOD,UAAU,YAAYE,OAAOC,QAAQ,CAACH,SAChD,IAAII,KAAKJ,OAAOK,WAAW,KAC3BC,OAAON;QACb,KAAK;YAAe;gBAClB,MAAMO,cAAcP;gBACpB,OAAOO,eACL,OAAOA,YAAYC,QAAQ,KAAK,YAChC,OAAOD,YAAYE,SAAS,KAAK,WAC/B,GAAGF,YAAYC,QAAQ,CAAC,EAAE,EAAED,YAAYE,SAAS,EAAE,GACnDH,OAAON;YACb;QACA,KAAK;QACL,KAAK;YACH,OAAOU,MAAMC,OAAO,CAACX,SAASA,MAAMY,IAAI,CAAC,QAAQN,OAAON;QAC1D,KAAK;YACH,IAAI;gBACF,OAAOa,KAAKC,SAAS,CAACd;YACxB,EAAE,eAAM;gBACN,OAAOM,OAAON;YAChB;QACF;YACE,OAAOM,OAAON;IAClB;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASe,iBAAiBC,KAAmB;IAClD,OAAOA,MAAMC,KAAK,CAACC,GAAG,CAACrB,WAAWe,IAAI,CAAC;AACzC;AAEA,sEAAsE,GACtE,OAAO,SAASO,cACdH,KAAmB,EACnBI,GAA4B;IAE5B,OAAOJ,MAAMC,KAAK,CACfC,GAAG,CAAC,CAACG;QACJ,MAAMtB,QAAQiB,MAAMM,MAAM,CAACD,QAAQ;QACnC,OAAOxB,UAAUE,QAAQD,sBAAsBC,OAAOqB,GAAG,CAACC,QAAQ,IAAI;IACxE,GACCT,IAAI,CAAC;AACV;AAEA,0EAA0E,GAC1E,OAAO,SAASW,oBACdP,KAAmB,EACnBI,GAA4B;IAE5B,OAAOI,OAAOC,WAAW,CACvBT,MAAMC,KAAK,CAACC,GAAG,CAAC,CAACG;QACf,MAAMtB,QAAQiB,MAAMM,MAAM,CAACD,QAAQ;QACnC,OAAO;YAACA;YAAStB,QAAQD,sBAAsBC,OAAOqB,GAAG,CAACC,QAAQ,IAAI;SAAG;IAC3E;AAEJ;AAEA,oEAAoE,GACpE,OAAO,SAASK,oBACdV,KAAmB,EACnBW,IAAoC;IAEpC,OAAO;QACLZ,iBAAiBC;WACdW,KAAKT,GAAG,CAAC,CAACE,MAAQD,cAAcH,OAAOI;KAC3C,CAACR,IAAI,CAAC;AACT"}
|
|
@@ -17,15 +17,16 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* Dataset CSV/JSON round-tripping (AGL-182) — the IMPORT half.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
* here
|
|
24
|
-
* for everything dataset-io. What stays here is the part that is
|
|
25
|
-
* DATASETS — mapping a file's columns onto a `DatasetModel`.
|
|
20
|
+
* The serialization half is `dataset-csv.ts` beside this, and the CSV parser
|
|
21
|
+
* and the row-count check are the platform's `app-utils/csv` (AGL-2335), so
|
|
22
|
+
* the email, commerce and CRM imports read files with the same parser; all of
|
|
23
|
+
* them are re-exported here, so `@aglyn/plugins-data/model` remains the one
|
|
24
|
+
* import site for everything dataset-io. What stays here is the part that is
|
|
25
|
+
* about DATASETS — mapping a file's columns onto a `DatasetModel`.
|
|
26
26
|
*/
|
|
27
27
|
import type { DatasetModel } from '@aglyn/aglyn';
|
|
28
|
-
export { countCsvDataRows,
|
|
28
|
+
export { countCsvDataRows, exportShortfall, parseCsv, } from '@aglyn/aglyn/app-utils/csv';
|
|
29
|
+
export { datasetCsvHeader, datasetCsvRow, datasetRecordToJson, datasetRecordsToCsv, serializeDatasetValue, } from './dataset-csv';
|
|
29
30
|
/**
|
|
30
31
|
* Parses pasted/uploaded import text — a JSON array of objects or CSV
|
|
31
32
|
* with a header row — into raw string maps keyed by the source column
|
|
@@ -16,14 +16,15 @@
|
|
|
16
16
|
*/ /**
|
|
17
17
|
* Dataset CSV/JSON round-tripping (AGL-182) — the IMPORT half.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
* here
|
|
23
|
-
* for everything dataset-io. What stays here is the part that is
|
|
24
|
-
* DATASETS — mapping a file's columns onto a `DatasetModel`.
|
|
25
|
-
*/ import { parseCsv } from "@aglyn/aglyn/app-utils/
|
|
26
|
-
export { countCsvDataRows,
|
|
19
|
+
* The serialization half is `dataset-csv.ts` beside this, and the CSV parser
|
|
20
|
+
* and the row-count check are the platform's `app-utils/csv` (AGL-2335), so
|
|
21
|
+
* the email, commerce and CRM imports read files with the same parser; all of
|
|
22
|
+
* them are re-exported here, so `@aglyn/plugins-data/model` remains the one
|
|
23
|
+
* import site for everything dataset-io. What stays here is the part that is
|
|
24
|
+
* about DATASETS — mapping a file's columns onto a `DatasetModel`.
|
|
25
|
+
*/ import { parseCsv } from "@aglyn/aglyn/app-utils/csv";
|
|
26
|
+
export { countCsvDataRows, exportShortfall, parseCsv } from "@aglyn/aglyn/app-utils/csv";
|
|
27
|
+
export { datasetCsvHeader, datasetCsvRow, datasetRecordToJson, datasetRecordsToCsv, serializeDatasetValue } from "./dataset-csv.js";
|
|
27
28
|
/**
|
|
28
29
|
* Parses pasted/uploaded import text — a JSON array of objects or CSV
|
|
29
30
|
* with a header row — into raw string maps keyed by the source column
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-io.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\n/**\n * Dataset CSV/JSON round-tripping (AGL-182) — the IMPORT half.\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/data/src/lib/model/dataset-io.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\n/**\n * Dataset CSV/JSON round-tripping (AGL-182) — the IMPORT half.\n *\n * The serialization half is `dataset-csv.ts` beside this, and the CSV parser\n * and the row-count check are the platform's `app-utils/csv` (AGL-2335), so\n * the email, commerce and CRM imports read files with the same parser; all of\n * them are re-exported here, so `@aglyn/plugins-data/model` remains the one\n * import site for everything dataset-io. What stays here is the part that is\n * about DATASETS — mapping a file's columns onto a `DatasetModel`.\n */\n\nimport type { DatasetModel } from '@aglyn/aglyn'\nimport { parseCsv } from '@aglyn/aglyn/app-utils/csv'\nexport {\n countCsvDataRows,\n exportShortfall,\n parseCsv,\n} from '@aglyn/aglyn/app-utils/csv'\nexport {\n datasetCsvHeader,\n datasetCsvRow,\n datasetRecordToJson,\n datasetRecordsToCsv,\n serializeDatasetValue,\n} from './dataset-csv'\n\n/**\n * Parses pasted/uploaded import text — a JSON array of objects or CSV\n * with a header row — into raw string maps keyed by the source column\n * names. Returns null when nothing parseable is found.\n */\nexport function parseImportRows(\n text: string,\n): Array<Record<string, string>> | null {\n const trimmed = String(text ?? '').trim()\n if (!trimmed) return null\n if (trimmed.startsWith('[') || trimmed.startsWith('{')) {\n try {\n const parsed = JSON.parse(trimmed)\n const list = Array.isArray(parsed) ? parsed : [parsed]\n return list\n .filter((entry) => entry && typeof entry === 'object')\n .map((entry) =>\n Object.fromEntries(\n Object.entries(entry as Record<string, unknown>).map(\n ([key, value]) => [\n key,\n typeof value === 'string' ? value : JSON.stringify(value),\n ],\n ),\n ),\n )\n } catch {\n return null\n }\n }\n const rows = parseCsv(trimmed)\n if (rows.length < 2) return null\n const [header, ...body] = rows\n return body.map((cells) =>\n Object.fromEntries(\n header.map((column, index) => [column.trim(), cells[index] ?? '']),\n ),\n )\n}\n\n/**\n * Maps raw import columns to model fieldIds: exact fieldId match first,\n * then case-insensitive display-name match. Unmatched columns are\n * reported, never silently dropped.\n */\nexport function mapImportColumns(\n model: DatasetModel,\n columns: string[],\n): { mapping: Record<string, string>; unmatched: string[] } {\n const byName = new Map<string, string>()\n for (const fieldId of model.order) {\n byName.set(fieldId.toLowerCase(), fieldId)\n const display = model.fields[fieldId]?.name\n if (display) byName.set(display.trim().toLowerCase(), fieldId)\n }\n const mapping: Record<string, string> = {}\n const unmatched: string[] = []\n for (const column of columns) {\n const fieldId = byName.get(column.trim().toLowerCase())\n if (fieldId) mapping[column] = fieldId\n else unmatched.push(column)\n }\n return { mapping, unmatched }\n}\n"],"names":["parseCsv","countCsvDataRows","exportShortfall","datasetCsvHeader","datasetCsvRow","datasetRecordToJson","datasetRecordsToCsv","serializeDatasetValue","parseImportRows","text","trimmed","String","trim","startsWith","parsed","JSON","parse","list","Array","isArray","filter","entry","map","Object","fromEntries","entries","key","value","stringify","rows","length","header","body","cells","column","index","mapImportColumns","model","columns","byName","Map","fieldId","order","set","toLowerCase","display","fields","name","mapping","unmatched","get","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAGD,SAASA,QAAQ,QAAQ,6BAA4B;AACrD,SACEC,gBAAgB,EAChBC,eAAe,EACfF,QAAQ,QACH,6BAA4B;AACnC,SACEG,gBAAgB,EAChBC,aAAa,EACbC,mBAAmB,EACnBC,mBAAmB,EACnBC,qBAAqB,QAChB,mBAAe;AAEtB;;;;CAIC,GACD,OAAO,SAASC,gBACdC,IAAY;IAEZ,MAAMC,UAAUC,OAAOF,eAAAA,OAAQ,IAAIG,IAAI;IACvC,IAAI,CAACF,SAAS,OAAO;IACrB,IAAIA,QAAQG,UAAU,CAAC,QAAQH,QAAQG,UAAU,CAAC,MAAM;QACtD,IAAI;YACF,MAAMC,SAASC,KAAKC,KAAK,CAACN;YAC1B,MAAMO,OAAOC,MAAMC,OAAO,CAACL,UAAUA,SAAS;gBAACA;aAAO;YACtD,OAAOG,KACJG,MAAM,CAAC,CAACC,QAAUA,SAAS,OAAOA,UAAU,UAC5CC,GAAG,CAAC,CAACD,QACJE,OAAOC,WAAW,CAChBD,OAAOE,OAAO,CAACJ,OAAkCC,GAAG,CAClD,CAAC,CAACI,KAAKC,MAAM,GAAK;wBAChBD;wBACA,OAAOC,UAAU,WAAWA,QAAQZ,KAAKa,SAAS,CAACD;qBACpD;QAIX,EAAE,eAAM;YACN,OAAO;QACT;IACF;IACA,MAAME,OAAO7B,SAASU;IACtB,IAAImB,KAAKC,MAAM,GAAG,GAAG,OAAO;IAC5B,MAAM,CAACC,QAAQ,GAAGC,KAAK,GAAGH;IAC1B,OAAOG,KAAKV,GAAG,CAAC,CAACW,QACfV,OAAOC,WAAW,CAChBO,OAAOT,GAAG,CAAC,CAACY,QAAQC;gBAA0BF;mBAAhB;gBAACC,OAAOtB,IAAI;iBAAIqB,eAAAA,KAAK,CAACE,MAAM,YAAZF,eAAgB;aAAG;;AAGvE;AAEA;;;;CAIC,GACD,OAAO,SAASG,iBACdC,KAAmB,EACnBC,OAAiB;IAEjB,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,WAAWJ,MAAMK,KAAK,CAAE;YAEjBL;QADhBE,OAAOI,GAAG,CAACF,QAAQG,WAAW,IAAIH;QAClC,MAAMI,WAAUR,wBAAAA,MAAMS,MAAM,CAACL,QAAQ,qBAArBJ,sBAAuBU,IAAI;QAC3C,IAAIF,SAASN,OAAOI,GAAG,CAACE,QAAQjC,IAAI,GAAGgC,WAAW,IAAIH;IACxD;IACA,MAAMO,UAAkC,CAAC;IACzC,MAAMC,YAAsB,EAAE;IAC9B,KAAK,MAAMf,UAAUI,QAAS;QAC5B,MAAMG,UAAUF,OAAOW,GAAG,CAAChB,OAAOtB,IAAI,GAAGgC,WAAW;QACpD,IAAIH,SAASO,OAAO,CAACd,OAAO,GAAGO;aAC1BQ,UAAUE,IAAI,CAACjB;IACtB;IACA,OAAO;QAAEc;QAASC;IAAU;AAC9B"}
|