@mulmoclaude/core 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/billing-clients-worklog.md +3 -2
- package/assets/helps/collection-skills.md +54 -1
- package/assets/helps/custom-view.md +37 -3
- package/assets/helps/error-recovery.md +30 -0
- package/assets/helps/todo-collection.md +2 -2
- package/dist/collection/core/fieldText.d.ts +11 -0
- package/dist/collection/core/project.d.ts +3 -0
- package/dist/collection/core/schema.d.ts +13 -1
- package/dist/collection/core/schemaZ.d.ts +16 -0
- package/dist/collection/index.cjs +18 -10
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +15 -11
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/csvStore.d.ts +8 -1
- package/dist/collection/server/discoveredCollection.d.ts +4 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/index.cjs +20 -17
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/queryRunner.d.ts +4 -0
- package/dist/collection/server/spawn.d.ts +2 -1
- package/dist/collection/server/sqliteStore.d.ts +14 -0
- package/dist/collection/server/store.d.ts +43 -8
- package/dist/collection/server/storePage.d.ts +32 -0
- package/dist/collection-watchers/index.cjs +167 -43
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.d.ts +1 -1
- package/dist/collection-watchers/index.js +168 -45
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +13 -11
- package/dist/collection-watchers/watcher.d.ts +4 -3
- package/dist/{discovery-3K_Wtkvn.js → discovery-QUi_7aNG.js} +882 -7
- package/dist/discovery-QUi_7aNG.js.map +1 -0
- package/dist/{discovery-BZ2ms1tE.cjs → discovery-cwtfyDxl.cjs} +994 -4
- package/dist/discovery-cwtfyDxl.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +26 -14
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +26 -14
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/google/index.cjs +9 -11
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +9 -11
- package/dist/google/index.js.map +1 -1
- package/dist/{ids-BR4U1dJD.cjs → ids-Dl9Ij63K.cjs} +14 -1
- package/dist/ids-Dl9Ij63K.cjs.map +1 -0
- package/dist/{ids-D4e10971.js → ids-P2T89Gou.js} +9 -2
- package/dist/ids-P2T89Gou.js.map +1 -0
- package/dist/{ingestTypes-BsGOjR2l.js → ingestTypes-C-IQB4nw.js} +2 -2
- package/dist/{ingestTypes-BsGOjR2l.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
- package/dist/{ingestTypes-CygUXsjf.cjs → ingestTypes-DZpAC3N_.cjs} +2 -2
- package/dist/{ingestTypes-CygUXsjf.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
- package/dist/project-BWI5w_BT.cjs +17 -0
- package/dist/project-BWI5w_BT.cjs.map +1 -0
- package/dist/project-bU98ycsy.js +12 -0
- package/dist/project-bU98ycsy.js.map +1 -0
- package/dist/{promptSafety-Boz5qOs-.js → promptSafety-B5KPkaNY.js} +32 -9
- package/dist/promptSafety-B5KPkaNY.js.map +1 -0
- package/dist/{promptSafety-Co4cYQ_Z.cjs → promptSafety-BOc1B3im.cjs} +43 -8
- package/dist/promptSafety-BOc1B3im.cjs.map +1 -0
- package/dist/remote-view/index.cjs +5 -3
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +3 -1
- package/dist/remote-view/index.js +5 -3
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-DLRz-U7F.cjs → server-B7mtx4Cd.cjs} +302 -785
- package/dist/server-B7mtx4Cd.cjs.map +1 -0
- package/dist/{server-DZH7OjOD.js → server-CNLZZONa.js} +292 -696
- package/dist/server-CNLZZONa.js.map +1 -0
- package/package.json +1 -1
- package/dist/discovery-3K_Wtkvn.js.map +0 -1
- package/dist/discovery-BZ2ms1tE.cjs.map +0 -1
- package/dist/ids-BR4U1dJD.cjs.map +0 -1
- package/dist/ids-D4e10971.js.map +0 -1
- package/dist/promptSafety-Boz5qOs-.js.map +0 -1
- package/dist/promptSafety-Co4cYQ_Z.cjs.map +0 -1
- package/dist/server-DLRz-U7F.cjs.map +0 -1
- package/dist/server-DZH7OjOD.js.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
|
|
2
|
-
const require_ids = require("./ids-
|
|
2
|
+
const require_ids = require("./ids-Dl9Ij63K.cjs");
|
|
3
|
+
const require_project = require("./project-BWI5w_BT.cjs");
|
|
3
4
|
const require_templatePath = require("./templatePath-27vUZowm.cjs");
|
|
4
5
|
let node_path = require("node:path");
|
|
5
6
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
7
|
let node_fs = require("node:fs");
|
|
7
8
|
let node_fs_promises = require("node:fs/promises");
|
|
8
9
|
let node_crypto = require("node:crypto");
|
|
10
|
+
let node_os = require("node:os");
|
|
11
|
+
let iconv_lite = require("iconv-lite");
|
|
12
|
+
iconv_lite = require_rolldown_runtime.__toESM(iconv_lite, 1);
|
|
9
13
|
let zod = require("zod");
|
|
10
14
|
//#region src/collection/server/host.ts
|
|
11
15
|
var current = null;
|
|
@@ -635,6 +639,843 @@ ${dataJson}
|
|
|
635
639
|
|
|
636
640
|
${templateText}`;
|
|
637
641
|
}
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/collection/core/queryZ.ts
|
|
644
|
+
/** Result-column aliases double as SQL identifiers and JSON keys — keep
|
|
645
|
+
* them to a conservative identifier charset so neither side needs
|
|
646
|
+
* escaping gymnastics. */
|
|
647
|
+
var SAFE_ALIAS_PATTERN = /^[A-Za-z_]\w{0,63}$/;
|
|
648
|
+
/** Hard ceiling on returned rows; `limit` clamps below it. A group-by on
|
|
649
|
+
* a near-unique column would otherwise return one row per source row —
|
|
650
|
+
* the exact materialization the aggregate path exists to avoid. */
|
|
651
|
+
var MAX_QUERY_ROWS = 1e4;
|
|
652
|
+
/** Default row cap when the query declares no `limit`. */
|
|
653
|
+
var DEFAULT_QUERY_ROWS = 1e3;
|
|
654
|
+
/** One aggregate column: `count` (rows; `column` optional to count
|
|
655
|
+
* non-null cells) or `sum`/`avg`/`min`/`max` over a named CSV column. */
|
|
656
|
+
var QueryAggregateZ = zod.z.object({
|
|
657
|
+
op: zod.z.enum([
|
|
658
|
+
"count",
|
|
659
|
+
"sum",
|
|
660
|
+
"avg",
|
|
661
|
+
"min",
|
|
662
|
+
"max"
|
|
663
|
+
]),
|
|
664
|
+
column: zod.z.string().min(1).optional()
|
|
665
|
+
}).refine((aggregate) => aggregate.op === "count" || aggregate.column !== void 0, {
|
|
666
|
+
message: "`column` is required for every aggregate op except `count`",
|
|
667
|
+
path: ["column"]
|
|
668
|
+
});
|
|
669
|
+
/** One filter condition. Same op vocabulary as the schema-level `where`
|
|
670
|
+
* (`core/where.ts`) so authors learn one set; values may be typed
|
|
671
|
+
* (number / boolean) since CSV columns are. `in` requires an array
|
|
672
|
+
* value, every other op a scalar. */
|
|
673
|
+
var QueryWhereZ = zod.z.object({
|
|
674
|
+
field: zod.z.string().min(1),
|
|
675
|
+
op: zod.z.enum([
|
|
676
|
+
"eq",
|
|
677
|
+
"ne",
|
|
678
|
+
"in",
|
|
679
|
+
"gt",
|
|
680
|
+
"gte",
|
|
681
|
+
"lt",
|
|
682
|
+
"lte",
|
|
683
|
+
"contains"
|
|
684
|
+
]),
|
|
685
|
+
value: zod.z.union([
|
|
686
|
+
zod.z.string(),
|
|
687
|
+
zod.z.number(),
|
|
688
|
+
zod.z.boolean(),
|
|
689
|
+
zod.z.array(zod.z.union([
|
|
690
|
+
zod.z.string(),
|
|
691
|
+
zod.z.number(),
|
|
692
|
+
zod.z.boolean()
|
|
693
|
+
])).min(1).max(100)
|
|
694
|
+
])
|
|
695
|
+
}).refine((cond) => cond.op === "in" === Array.isArray(cond.value), {
|
|
696
|
+
message: "`in` requires an array value (the allowed set); every other op requires a scalar value",
|
|
697
|
+
path: ["value"]
|
|
698
|
+
});
|
|
699
|
+
var QueryOrderZ = zod.z.object({
|
|
700
|
+
/** A `groupBy` column or an aggregate alias — membership enforced by
|
|
701
|
+
* the whole-query refine below. */
|
|
702
|
+
field: zod.z.string().min(1),
|
|
703
|
+
dir: zod.z.enum(["asc", "desc"]).optional()
|
|
704
|
+
});
|
|
705
|
+
/** The whole query. At least one of `groupBy` / `aggregates` must be
|
|
706
|
+
* present: bare `groupBy` is a DISTINCT listing, bare `aggregates` a
|
|
707
|
+
* whole-file scalar row, together a grouped aggregation. */
|
|
708
|
+
var CollectionQueryZ = zod.z.object({
|
|
709
|
+
groupBy: zod.z.array(zod.z.string().min(1)).max(8).refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { message: "`groupBy` columns must be unique (case-insensitively — SQL identifiers ignore case)" }).optional(),
|
|
710
|
+
aggregates: zod.z.record(zod.z.string().regex(SAFE_ALIAS_PATTERN, "aggregate aliases must be simple identifiers (letters/digits/underscore)"), QueryAggregateZ).optional(),
|
|
711
|
+
where: zod.z.array(QueryWhereZ).max(16).optional(),
|
|
712
|
+
orderBy: zod.z.array(QueryOrderZ).max(4).optional(),
|
|
713
|
+
limit: zod.z.number().int().min(1).max(MAX_QUERY_ROWS).optional()
|
|
714
|
+
}).refine((query) => (query.groupBy?.length ?? 0) > 0 || Object.keys(query.aggregates ?? {}).length > 0, {
|
|
715
|
+
message: "declare at least one of `groupBy` (columns to bucket by) or `aggregates` (values to compute)",
|
|
716
|
+
path: ["groupBy"]
|
|
717
|
+
}).refine((query) => Object.keys(query.aggregates ?? {}).length <= 32, {
|
|
718
|
+
message: `\`aggregates\` supports at most 32 entries`,
|
|
719
|
+
path: ["aggregates"]
|
|
720
|
+
}).refine((query) => {
|
|
721
|
+
const groupLower = new Set((query.groupBy ?? []).map((column) => column.toLowerCase()));
|
|
722
|
+
const seen = /* @__PURE__ */ new Set();
|
|
723
|
+
return Object.keys(query.aggregates ?? {}).every((alias) => {
|
|
724
|
+
const lower = alias.toLowerCase();
|
|
725
|
+
if (groupLower.has(lower) || seen.has(lower)) return false;
|
|
726
|
+
seen.add(lower);
|
|
727
|
+
return true;
|
|
728
|
+
});
|
|
729
|
+
}, {
|
|
730
|
+
message: "aggregate aliases must be unique and must not collide with `groupBy` column names (case-insensitively — SQL identifiers ignore case)",
|
|
731
|
+
path: ["aggregates"]
|
|
732
|
+
}).refine((query) => {
|
|
733
|
+
const sortable = /* @__PURE__ */ new Set([...query.groupBy ?? [], ...Object.keys(query.aggregates ?? {})]);
|
|
734
|
+
return (query.orderBy ?? []).every((order) => sortable.has(order.field));
|
|
735
|
+
}, {
|
|
736
|
+
message: "every `orderBy.field` must be a `groupBy` column or an aggregate alias",
|
|
737
|
+
path: ["orderBy"]
|
|
738
|
+
});
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/collection/server/csvQuery.ts
|
|
741
|
+
/** Double-quote a SQL identifier (CSV column name / result alias). */
|
|
742
|
+
function quoteIdent(name) {
|
|
743
|
+
return `"${name.replaceAll("\"", "\"\"")}"`;
|
|
744
|
+
}
|
|
745
|
+
/** Single-quote a SQL string literal (a `types={...}` struct key). */
|
|
746
|
+
function quoteLiteral(value) {
|
|
747
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
748
|
+
}
|
|
749
|
+
/** The `read_csv` argument list shared by every CSV query: the (prepared)
|
|
750
|
+
* path plus a `types` pin forcing the key column to VARCHAR — without it
|
|
751
|
+
* DuckDB's sniffer turns `001` into BIGINT 1, so leading zeros vanish
|
|
752
|
+
* and distinct keys collapse. */
|
|
753
|
+
function readCsvArgs(primaryKey) {
|
|
754
|
+
return `?, types={${quoteLiteral(primaryKey)}: 'VARCHAR'}`;
|
|
755
|
+
}
|
|
756
|
+
/** One aggregate's SQL expression. `sum`/`avg` TRY_CAST to DOUBLE so a
|
|
757
|
+
* column the sniffer kept as VARCHAR (mixed values) aggregates over its
|
|
758
|
+
* numeric cells instead of erroring; non-numeric cells become NULL and
|
|
759
|
+
* are skipped — standard BI tolerance. `min`/`max` stay native (they are
|
|
760
|
+
* meaningful on strings and dates too). */
|
|
761
|
+
function aggregateExpr(aggregate) {
|
|
762
|
+
const { op, column } = aggregate;
|
|
763
|
+
if (op === "count") return column === void 0 ? "count(*)" : `count(${quoteIdent(column)})`;
|
|
764
|
+
if (op === "sum" || op === "avg") return `${op}(TRY_CAST(${quoteIdent(column ?? "")} AS DOUBLE))`;
|
|
765
|
+
return `${op}(${quoteIdent(column ?? "")})`;
|
|
766
|
+
}
|
|
767
|
+
/** One where condition → SQL fragment + its bound parameters. String
|
|
768
|
+
* equality compares against `CAST(col AS VARCHAR)` so a sniffer-typed
|
|
769
|
+
* column still matches its textual value; numeric/boolean values compare
|
|
770
|
+
* natively (DuckDB coerces the column side). */
|
|
771
|
+
function whereFragment(cond) {
|
|
772
|
+
const column = quoteIdent(cond.field);
|
|
773
|
+
const asText = `CAST(${column} AS VARCHAR)`;
|
|
774
|
+
if (cond.op === "in") {
|
|
775
|
+
const values = cond.value;
|
|
776
|
+
return {
|
|
777
|
+
sql: `${values.every((value) => typeof value === "string") ? asText : column} IN (${values.map(() => "?").join(", ")})`,
|
|
778
|
+
params: values
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (cond.op === "contains") return {
|
|
782
|
+
sql: `contains(${asText}, ?)`,
|
|
783
|
+
params: [String(cond.value)]
|
|
784
|
+
};
|
|
785
|
+
const operator = {
|
|
786
|
+
eq: "=",
|
|
787
|
+
ne: "<>",
|
|
788
|
+
gt: ">",
|
|
789
|
+
gte: ">=",
|
|
790
|
+
lt: "<",
|
|
791
|
+
lte: "<="
|
|
792
|
+
}[cond.op];
|
|
793
|
+
return {
|
|
794
|
+
sql: `${typeof cond.value === "string" && (cond.op === "eq" || cond.op === "ne") ? asText : column} ${operator} ?`,
|
|
795
|
+
params: [cond.value]
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/** Compile a validated query against `fromSql` (a table-function call
|
|
799
|
+
* whose FIRST placeholder is the source path — the executor binds it).
|
|
800
|
+
* Returns the SQL and the where-value parameters that follow the path.
|
|
801
|
+
* Callers MUST have run `CollectionQueryZ` first; this function trusts
|
|
802
|
+
* the shape (aliases already charset-checked, orderBy membership already
|
|
803
|
+
* enforced). */
|
|
804
|
+
function compileQuery(query, fromSql) {
|
|
805
|
+
const groupBy = query.groupBy ?? [];
|
|
806
|
+
const aggregates = Object.entries(query.aggregates ?? {});
|
|
807
|
+
const selectList = [...groupBy.map(quoteIdent), ...aggregates.map(([alias, aggregate]) => `${aggregateExpr(aggregate)} AS ${quoteIdent(alias)}`)];
|
|
808
|
+
const where = (query.where ?? []).map(whereFragment);
|
|
809
|
+
const clauses = [`SELECT ${selectList.join(", ")}`, `FROM ${fromSql}`];
|
|
810
|
+
if (where.length > 0) clauses.push(`WHERE ${where.map((fragment) => fragment.sql).join(" AND ")}`);
|
|
811
|
+
if (groupBy.length > 0) clauses.push(`GROUP BY ${groupBy.map(quoteIdent).join(", ")}`);
|
|
812
|
+
const orderBy = (query.orderBy ?? []).map((order) => quoteIdent(order.field) + (order.dir === "desc" ? " DESC" : " ASC"));
|
|
813
|
+
if (orderBy.length > 0) clauses.push(`ORDER BY ${orderBy.join(", ")}`);
|
|
814
|
+
clauses.push(`LIMIT ${query.limit ?? 1e3}`);
|
|
815
|
+
return {
|
|
816
|
+
sql: clauses.join(" "),
|
|
817
|
+
params: where.flatMap((fragment) => fragment.params)
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/** Compile against a CSV file (the dataSource store's engine). */
|
|
821
|
+
function compileCsvQuery(query, primaryKey) {
|
|
822
|
+
return compileQuery(query, `read_csv(${readCsvArgs(primaryKey)})`);
|
|
823
|
+
}
|
|
824
|
+
/** Compile against a JSONL file of ENRICHED records — the file-backed
|
|
825
|
+
* collections' engine (see `jsonlQuery.ts`). No VARCHAR key pin needed:
|
|
826
|
+
* enriched record ids are already strings. `sample_size=-1` makes the
|
|
827
|
+
* schema inference scan EVERY line — with the default sample, a sparse
|
|
828
|
+
* optional/derived field first appearing past the sample would not be
|
|
829
|
+
* inferred as a column and the query would binder-error on it (Codex P2
|
|
830
|
+
* on #2165). The full scan costs nothing extra here: aggregation reads
|
|
831
|
+
* the whole file anyway. */
|
|
832
|
+
function compileJsonlQuery(query) {
|
|
833
|
+
return compileQuery(query, `read_json(?, format='newline_delimited', sample_size=-1)`);
|
|
834
|
+
}
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/collection/server/csvStore.ts
|
|
837
|
+
/** `list()` row cap. Over-cap files are truncated with a warn — the v1
|
|
838
|
+
* contract is "browse + per-record views", not full-table analytics. */
|
|
839
|
+
var MAX_CSV_ROWS = 5e3;
|
|
840
|
+
/** Record ids minted from non-safe key values: `id0x` + utf-8 hex. Raw key
|
|
841
|
+
* values that themselves match this pattern are ALSO encoded, so the
|
|
842
|
+
* encoded namespace never collides with a raw value (injective mapping). */
|
|
843
|
+
var ENCODED_ID_PATTERN = /^id0x([0-9a-f]+)$/;
|
|
844
|
+
/** A CSV key value → the record id it's addressed by. Safe values pass
|
|
845
|
+
* through untouched; everything else (and anything shaped like an encoded
|
|
846
|
+
* id) becomes `id0x<hex>`. Pure + exported for unit tests. */
|
|
847
|
+
function encodeCsvRecordId(rawKey) {
|
|
848
|
+
if (safeRecordId(rawKey) === rawKey && !ENCODED_ID_PATTERN.test(rawKey)) return rawKey;
|
|
849
|
+
return `id0x${Buffer.from(rawKey, "utf-8").toString("hex")}`;
|
|
850
|
+
}
|
|
851
|
+
/** A record id → the CSV key value to look up. Inverse of
|
|
852
|
+
* `encodeCsvRecordId` for encoded ids; anything else is already the raw
|
|
853
|
+
* value. Pure + exported for unit tests. */
|
|
854
|
+
function decodeCsvRecordId(itemId) {
|
|
855
|
+
const match = ENCODED_ID_PATTERN.exec(itemId);
|
|
856
|
+
if (!match) return itemId;
|
|
857
|
+
return Buffer.from(match[1], "hex").toString("utf-8");
|
|
858
|
+
}
|
|
859
|
+
/** Normalize one DuckDB JS value into a JSON-safe record value: BigInt →
|
|
860
|
+
* number (string beyond the safe range), DATE/TIMESTAMP → ISO string
|
|
861
|
+
* (date-only when the clock is exactly UTC midnight, matching the `date`
|
|
862
|
+
* field contract), exotic DuckDB values → their string form. Pure +
|
|
863
|
+
* exported for unit tests. */
|
|
864
|
+
function normalizeCsvValue(value) {
|
|
865
|
+
if (typeof value === "bigint") return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(-Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
866
|
+
if (value instanceof Date) {
|
|
867
|
+
const iso = value.toISOString();
|
|
868
|
+
return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
|
|
869
|
+
}
|
|
870
|
+
if (value !== null && typeof value === "object") return String(value);
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
/** One raw DuckDB row → a CollectionItem, or null when the key cell is
|
|
874
|
+
* missing/empty (the row can't be addressed). The primaryKey field is
|
|
875
|
+
* OVERWRITTEN with the (possibly encoded) record id so `item[primaryKey]`
|
|
876
|
+
* and the record's address never drift — same invariant the file store's
|
|
877
|
+
* write path enforces. Pure + exported for unit tests. */
|
|
878
|
+
function csvRowToItem(row, primaryKey) {
|
|
879
|
+
const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
|
|
880
|
+
const rawKey = normalized[primaryKey];
|
|
881
|
+
if (rawKey === null || rawKey === void 0 || String(rawKey) === "") return null;
|
|
882
|
+
return {
|
|
883
|
+
...normalized,
|
|
884
|
+
[primaryKey]: encodeCsvRecordId(String(rawKey))
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/** Dedupe by record id, LAST row wins (matches `csvRead`'s last-match
|
|
888
|
+
* pick). Returns the surviving items in first-seen order. Pure +
|
|
889
|
+
* exported for unit tests. */
|
|
890
|
+
function dedupeByRecordId(items, primaryKey) {
|
|
891
|
+
const byId = /* @__PURE__ */ new Map();
|
|
892
|
+
for (const item of items) byId.set(String(item[primaryKey]), item);
|
|
893
|
+
return {
|
|
894
|
+
items: [...byId.values()],
|
|
895
|
+
duplicates: items.length - byId.size
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/** True when a thrown DuckDB error is the `types` pin naming a column the
|
|
899
|
+
* CSV doesn't have — the schema/file-mismatch case the caller downgrades
|
|
900
|
+
* to "empty collection + warn" instead of a 500. */
|
|
901
|
+
function isMissingKeyColumnError(err) {
|
|
902
|
+
return String(err).includes("do not exist in the CSV");
|
|
903
|
+
}
|
|
904
|
+
/** Bytes sniffed for UTF-8 validity. The trailing 3 bytes of the sample
|
|
905
|
+
* are dropped so a multibyte char split at the boundary can't produce a
|
|
906
|
+
* false negative on a valid file. */
|
|
907
|
+
var SNIFF_BYTES = 1024 * 1024;
|
|
908
|
+
function isValidUtf8(buf) {
|
|
909
|
+
try {
|
|
910
|
+
new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
911
|
+
return true;
|
|
912
|
+
} catch {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/** Detect the (best-effort) encoding of a non-UTF-8 buffer. BOMs decide
|
|
917
|
+
* UTF-16; otherwise cp932 (the Shift_JIS superset — Excel-exported
|
|
918
|
+
* Japanese CSVs are the primary non-UTF-8 case this feature serves). */
|
|
919
|
+
function fallbackEncoding(buf) {
|
|
920
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) return "utf-16le";
|
|
921
|
+
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) return "utf-16be";
|
|
922
|
+
return "cp932";
|
|
923
|
+
}
|
|
924
|
+
function cacheDir() {
|
|
925
|
+
return node_path.default.join((0, node_os.tmpdir)(), "mulmoclaude-csv-utf8");
|
|
926
|
+
}
|
|
927
|
+
/** Read only the first `bytes` of a file — the encoding sniff must not
|
|
928
|
+
* pull a multi-hundred-MB CSV into memory on the (common) UTF-8 path. */
|
|
929
|
+
async function readHead(absPath, bytes) {
|
|
930
|
+
const handle = await (0, node_fs_promises.open)(absPath, "r");
|
|
931
|
+
try {
|
|
932
|
+
const { size } = await handle.stat();
|
|
933
|
+
const buf = Buffer.alloc(Math.min(bytes, size));
|
|
934
|
+
await handle.read(buf, 0, buf.length, 0);
|
|
935
|
+
return buf;
|
|
936
|
+
} finally {
|
|
937
|
+
await handle.close();
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/** Decode the whole file into a UTF-8 cache copy and return its path.
|
|
941
|
+
* Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
|
|
942
|
+
* unchanged one never does. */
|
|
943
|
+
async function pathExists(target) {
|
|
944
|
+
try {
|
|
945
|
+
await (0, node_fs_promises.stat)(target);
|
|
946
|
+
return true;
|
|
947
|
+
} catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/** Best-effort removal of older decode-cache entries for the same source
|
|
952
|
+
* path — a frequently-replaced large CSV would otherwise accumulate one
|
|
953
|
+
* full copy per (mtime, size) forever. Runs AFTER the current copy is
|
|
954
|
+
* published; a concurrent reader holding an old fd is unaffected
|
|
955
|
+
* (unlink-while-open is safe on POSIX). */
|
|
956
|
+
async function evictSupersededCache(key, keepBasename) {
|
|
957
|
+
try {
|
|
958
|
+
const entries = await (0, node_fs_promises.readdir)(cacheDir());
|
|
959
|
+
await Promise.all(entries.filter((name) => name.startsWith(`${key}-`) && name !== keepBasename).map((name) => (0, node_fs_promises.unlink)(node_path.default.join(cacheDir(), name)).catch(() => void 0)));
|
|
960
|
+
} catch {}
|
|
961
|
+
}
|
|
962
|
+
/** Decode the whole file into a UTF-8 cache copy and return its path.
|
|
963
|
+
* Cache key = (path, mtime, size), so a replaced CSV re-decodes and an
|
|
964
|
+
* unchanged one never does; superseded copies are evicted. The cache
|
|
965
|
+
* lives in the SHARED OS tmpdir, so the dir is 0700 and files 0600 —
|
|
966
|
+
* decoded rows must not be readable by other local users. */
|
|
967
|
+
async function decodeToCache(absPath, info) {
|
|
968
|
+
const key = (0, node_crypto.createHash)("sha256").update(absPath).digest("hex").slice(0, 16);
|
|
969
|
+
const cached = node_path.default.join(cacheDir(), `${key}-${Math.trunc(info.mtimeMs)}-${info.size}.csv`);
|
|
970
|
+
if (!await pathExists(cached)) {
|
|
971
|
+
const whole = await (0, node_fs_promises.readFile)(absPath);
|
|
972
|
+
const encoding = fallbackEncoding(whole);
|
|
973
|
+
const text = iconv_lite.default.decode(whole, encoding);
|
|
974
|
+
await (0, node_fs_promises.mkdir)(cacheDir(), {
|
|
975
|
+
recursive: true,
|
|
976
|
+
mode: 448
|
|
977
|
+
});
|
|
978
|
+
const tmp = `${cached}.${(0, node_crypto.randomBytes)(4).toString("hex")}.tmp`;
|
|
979
|
+
await (0, node_fs_promises.writeFile)(tmp, text, {
|
|
980
|
+
encoding: "utf-8",
|
|
981
|
+
mode: 384
|
|
982
|
+
});
|
|
983
|
+
await (0, node_fs_promises.rename)(tmp, cached);
|
|
984
|
+
log.info("collections", "decoded non-UTF-8 dataSource file to cache", {
|
|
985
|
+
path: absPath,
|
|
986
|
+
encoding
|
|
987
|
+
});
|
|
988
|
+
await evictSupersededCache(key, node_path.default.basename(cached));
|
|
989
|
+
}
|
|
990
|
+
return cached;
|
|
991
|
+
}
|
|
992
|
+
/** Re-validate the dataSource file at READ time, mirroring the JSON
|
|
993
|
+
* store's per-read defenses: realpath containment (a symlink swapped in
|
|
994
|
+
* after discovery must not walk out of the workspace) and an lstat
|
|
995
|
+
* regular-file check (a symlink leaf is refused outright, even one
|
|
996
|
+
* pointing inside the workspace — same rule as `isRegularFile` on
|
|
997
|
+
* record files). Returns the stat info, or null for "no readable file"
|
|
998
|
+
* (ENOENT / refused), which callers render as an empty collection. */
|
|
999
|
+
async function safeCsvStat(absPath, workspaceRoot) {
|
|
1000
|
+
if (!isContainedInRoot(absPath, workspaceRoot)) {
|
|
1001
|
+
log.warn("collections", "dataSource read refused: path escapes workspace", { path: absPath });
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
let info;
|
|
1005
|
+
try {
|
|
1006
|
+
info = await (0, node_fs_promises.lstat)(absPath);
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
if (err.code === "ENOENT") return null;
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
if (!info.isFile()) {
|
|
1012
|
+
log.warn("collections", "dataSource read refused: not a regular file (symlink?)", { path: absPath });
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
return info;
|
|
1016
|
+
}
|
|
1017
|
+
/** Return a path DuckDB can read as UTF-8: the original file when it
|
|
1018
|
+
* already is UTF-8 (the cheap, common case — only the head is sniffed),
|
|
1019
|
+
* else a decoded cache copy (see `decodeToCache`). Returns null when
|
|
1020
|
+
* there is no readable file (missing, symlink, or containment-refused —
|
|
1021
|
+
* see `safeCsvStat`), which callers render as an empty collection. */
|
|
1022
|
+
async function ensureUtf8CsvPath(absPath, workspaceRoot) {
|
|
1023
|
+
const info = await safeCsvStat(absPath, workspaceRoot);
|
|
1024
|
+
if (info === null) return null;
|
|
1025
|
+
const head = await readHead(absPath, SNIFF_BYTES);
|
|
1026
|
+
const sample = head.length === SNIFF_BYTES ? head.subarray(0, SNIFF_BYTES - 3) : head;
|
|
1027
|
+
if (!(head.length >= 2 && (head[0] === 255 && head[1] === 254 || head[0] === 254 && head[1] === 255)) && isValidUtf8(sample)) return absPath;
|
|
1028
|
+
return decodeToCache(absPath, info);
|
|
1029
|
+
}
|
|
1030
|
+
var instancePromise = null;
|
|
1031
|
+
/** Lazily create one shared in-memory DuckDB instance. The dynamic import
|
|
1032
|
+
* keeps the native module OUT of core's load path — a platform where the
|
|
1033
|
+
* prebuilt binding is missing degrades to a per-query error on dataSource
|
|
1034
|
+
* collections only, never a broken core. A failed init is retried on the
|
|
1035
|
+
* next call (the promise is reset). */
|
|
1036
|
+
async function duckDbInstance() {
|
|
1037
|
+
if (instancePromise === null) instancePromise = import("@duckdb/node-api").then((mod) => mod.DuckDBInstance.create(":memory:"));
|
|
1038
|
+
try {
|
|
1039
|
+
return await instancePromise;
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
instancePromise = null;
|
|
1042
|
+
throw new Error(`DuckDB is unavailable on this host (@duckdb/node-api failed to load: ${String(err)}) — dataSource collections cannot be read`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function queryCsv(sql, params) {
|
|
1046
|
+
const connection = await (await duckDbInstance()).connect();
|
|
1047
|
+
try {
|
|
1048
|
+
return (await connection.runAndReadAll(sql, params)).getRowObjectsJS();
|
|
1049
|
+
} finally {
|
|
1050
|
+
connection.disconnectSync();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function csvList(absPath, primaryKey, workspaceRoot) {
|
|
1054
|
+
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
|
|
1055
|
+
if (utf8Path === null) return {
|
|
1056
|
+
items: [],
|
|
1057
|
+
truncated: false
|
|
1058
|
+
};
|
|
1059
|
+
let rows;
|
|
1060
|
+
try {
|
|
1061
|
+
rows = await queryCsv(`SELECT * FROM read_csv(${readCsvArgs(primaryKey)}) LIMIT 5001`, [utf8Path]);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
if (!isMissingKeyColumnError(err)) throw err;
|
|
1064
|
+
log.warn("collections", "dataSource CSV has no primaryKey column — every row is skipped", {
|
|
1065
|
+
path: absPath,
|
|
1066
|
+
primaryKey
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
items: [],
|
|
1070
|
+
truncated: false
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const truncated = rows.length > MAX_CSV_ROWS;
|
|
1074
|
+
if (truncated) {
|
|
1075
|
+
log.warn("collections", "dataSource CSV truncated to row cap", {
|
|
1076
|
+
path: absPath,
|
|
1077
|
+
cap: MAX_CSV_ROWS
|
|
1078
|
+
});
|
|
1079
|
+
rows.length = MAX_CSV_ROWS;
|
|
1080
|
+
}
|
|
1081
|
+
const items = rows.map((row) => csvRowToItem(row, primaryKey)).filter((item) => item !== null);
|
|
1082
|
+
const skipped = rows.length - items.length;
|
|
1083
|
+
if (skipped > 0) log.warn("collections", "dataSource CSV rows skipped (empty key cell)", {
|
|
1084
|
+
path: absPath,
|
|
1085
|
+
skipped
|
|
1086
|
+
});
|
|
1087
|
+
const deduped = dedupeByRecordId(items, primaryKey);
|
|
1088
|
+
if (deduped.duplicates > 0) log.warn("collections", "dataSource CSV has duplicate key values (last row wins)", {
|
|
1089
|
+
path: absPath,
|
|
1090
|
+
duplicates: deduped.duplicates
|
|
1091
|
+
});
|
|
1092
|
+
return {
|
|
1093
|
+
items: deduped.items,
|
|
1094
|
+
truncated
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/** The scan-order ordinal column the last-match read adds. Underscore
|
|
1098
|
+
* prefix keeps it out of any plausible CSV header namespace; it is
|
|
1099
|
+
* stripped from the returned record either way. */
|
|
1100
|
+
var ROW_ORDINAL = "__mc_row";
|
|
1101
|
+
/** One record by id. The comparison value rides as a prepared-statement
|
|
1102
|
+
* parameter, and the LAST matching row is selected IN DuckDB (scan-order
|
|
1103
|
+
* ordinal + LIMIT 1) — a CSV with thousands of duplicate keys must not
|
|
1104
|
+
* materialize them all for one detail read. Consistent with csvList's
|
|
1105
|
+
* last-wins dedupe. */
|
|
1106
|
+
async function csvRead(absPath, primaryKey, itemId, workspaceRoot) {
|
|
1107
|
+
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
|
|
1108
|
+
if (utf8Path === null) return null;
|
|
1109
|
+
const rawKey = decodeCsvRecordId(itemId);
|
|
1110
|
+
const last = (await queryCsv(`SELECT * FROM (SELECT *, row_number() OVER () AS ${quoteIdent(ROW_ORDINAL)} FROM read_csv(${readCsvArgs(primaryKey)})) WHERE CAST(${quoteIdent(primaryKey)} AS VARCHAR) = ? ORDER BY ${quoteIdent(ROW_ORDINAL)} DESC LIMIT 1`, [utf8Path, rawKey])).at(0);
|
|
1111
|
+
if (last === void 0) return null;
|
|
1112
|
+
const { [ROW_ORDINAL]: __ordinal, ...record } = last;
|
|
1113
|
+
return csvRowToItem(record, primaryKey);
|
|
1114
|
+
}
|
|
1115
|
+
/** Run a validated aggregation query (the structured DSL — see
|
|
1116
|
+
* `core/queryZ.ts`) over the WHOLE file: no row cap on the scan (a
|
|
1117
|
+
* capped aggregate would be a wrong number), only the result-row LIMIT
|
|
1118
|
+
* the compiler emits. Values are normalized like list/read rows so a
|
|
1119
|
+
* chart consumer gets plain JSON scalars. */
|
|
1120
|
+
async function csvRunQuery(absPath, primaryKey, query, workspaceRoot) {
|
|
1121
|
+
const utf8Path = await ensureUtf8CsvPath(absPath, workspaceRoot ?? getWorkspaceRoot());
|
|
1122
|
+
if (utf8Path === null) return [];
|
|
1123
|
+
const { sql, params } = compileCsvQuery(query, primaryKey);
|
|
1124
|
+
return (await queryCsv(sql, [utf8Path, ...params])).map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)])));
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/collection/server/storePage.ts
|
|
1128
|
+
/** Project `fields` (+ the primary key, always) out of each record. Thin
|
|
1129
|
+
* server-typed alias over the shared isomorphic `projectRecordFields`
|
|
1130
|
+
* (../core/project.ts) — kept as the store layer's exported name. */
|
|
1131
|
+
function projectItemFields(items, fields, primaryKey) {
|
|
1132
|
+
return require_project.projectRecordFields(items, fields, primaryKey);
|
|
1133
|
+
}
|
|
1134
|
+
/** Slice + project an already-ordered full read into a `ListPage` — the
|
|
1135
|
+
* shared emulation for stores without native paging. Pure, exported for
|
|
1136
|
+
* tests. `limit: 0` is a valid "count only" page. */
|
|
1137
|
+
function pageFromFullRead(items, opts, primaryKey, truncated) {
|
|
1138
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
1139
|
+
const end = opts.limit === void 0 ? void 0 : offset + Math.max(0, opts.limit);
|
|
1140
|
+
return {
|
|
1141
|
+
items: projectItemFields(items.slice(offset, end), opts.fields, primaryKey),
|
|
1142
|
+
total: items.length,
|
|
1143
|
+
truncated
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/collection/server/sqliteStore.ts
|
|
1148
|
+
var sqliteModule = null;
|
|
1149
|
+
/** Lazy-load node:sqlite once. A runtime without it (Node < 22.5) throws a
|
|
1150
|
+
* clearly-worded error the caller surfaces — never a bare MODULE_NOT_FOUND. */
|
|
1151
|
+
function loadSqlite() {
|
|
1152
|
+
sqliteModule ??= import("node:sqlite").then((mod) => mod, (err) => {
|
|
1153
|
+
sqliteModule = null;
|
|
1154
|
+
throw new Error(`sqlite storage needs the node:sqlite module (Node.js >= 22.5) — this runtime cannot load it: ${String(err)}`);
|
|
1155
|
+
});
|
|
1156
|
+
return sqliteModule;
|
|
1157
|
+
}
|
|
1158
|
+
/** The db file's on-disk state. A symlink or non-regular file is refused
|
|
1159
|
+
* (file-disclosure defense, same rule as io.ts record files); ENOENT is
|
|
1160
|
+
* just "no records yet". Any OTHER lstat failure (EACCES, EIO, …) is
|
|
1161
|
+
* rethrown so reads surface a real filesystem problem instead of
|
|
1162
|
+
* silently reporting an empty collection. */
|
|
1163
|
+
async function dbFileState(absPath) {
|
|
1164
|
+
try {
|
|
1165
|
+
return (await (0, node_fs_promises.lstat)(absPath)).isFile() ? "file" : "refused";
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (err.code === "ENOENT") return "missing";
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
var CREATE_TABLE = "CREATE TABLE IF NOT EXISTS records (id TEXT PRIMARY KEY, record TEXT NOT NULL)";
|
|
1172
|
+
/** Open the database for one operation, classifying the two unavailable
|
|
1173
|
+
* states so callers can map them honestly (`refused` ⇒ path-escape,
|
|
1174
|
+
* `missing` ⇒ empty / not-found — conflating them would misreport a
|
|
1175
|
+
* containment escape as "item not found"). The containment pre-check runs
|
|
1176
|
+
* BEFORE mkdir even when the file is missing — `isContainedInRoot`
|
|
1177
|
+
* resolves through the closest existing ancestor, so a symlinked-away
|
|
1178
|
+
* parent can never make the recursive mkdir create directories outside
|
|
1179
|
+
* the workspace (same pre/post belt-and-suspenders as io.ts writes). */
|
|
1180
|
+
async function openDb(absPath, workspaceRoot, mode) {
|
|
1181
|
+
const state = await dbFileState(absPath);
|
|
1182
|
+
if (state === "refused") {
|
|
1183
|
+
log.warn("collections", "sqlite database refused: not a regular file", { path: absPath });
|
|
1184
|
+
return { kind: "refused" };
|
|
1185
|
+
}
|
|
1186
|
+
if (!isContainedInRoot(node_path.default.dirname(absPath), workspaceRoot)) {
|
|
1187
|
+
log.warn("collections", "sqlite refused: database dir escapes workspace via symlink", { path: absPath });
|
|
1188
|
+
return { kind: "refused" };
|
|
1189
|
+
}
|
|
1190
|
+
if (mode === "read" && state === "missing") return { kind: "missing" };
|
|
1191
|
+
if (mode === "write") {
|
|
1192
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(absPath), { recursive: true });
|
|
1193
|
+
if (!isContainedInRoot(node_path.default.dirname(absPath), workspaceRoot)) {
|
|
1194
|
+
log.warn("collections", "sqlite write refused: database dir escapes workspace via symlink (post-mkdir)", { path: absPath });
|
|
1195
|
+
return { kind: "refused" };
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const { DatabaseSync } = await loadSqlite();
|
|
1199
|
+
const database = new DatabaseSync(absPath);
|
|
1200
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1201
|
+
database.exec(CREATE_TABLE);
|
|
1202
|
+
return {
|
|
1203
|
+
kind: "ok",
|
|
1204
|
+
database
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/** Run `operation` against the database and always close it; unavailable
|
|
1208
|
+
* states resolve through `onUnavailable` so each caller maps `missing`
|
|
1209
|
+
* vs `refused` to its own result kind. */
|
|
1210
|
+
async function withDb(absPath, workspaceRoot, mode, onUnavailable, operation) {
|
|
1211
|
+
const handle = await openDb(absPath, workspaceRoot, mode);
|
|
1212
|
+
if (handle.kind !== "ok") return onUnavailable(handle.kind);
|
|
1213
|
+
try {
|
|
1214
|
+
return await operation(handle.database);
|
|
1215
|
+
} finally {
|
|
1216
|
+
handle.database.close();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/** node:sqlite throws ERR_SQLITE_ERROR with the SQLite extended result
|
|
1220
|
+
* code on `errcode`. Our INSERT's duplicate-id failure is
|
|
1221
|
+
* SQLITE_CONSTRAINT_PRIMARYKEY (1555); SQLITE_CONSTRAINT_UNIQUE (2067)
|
|
1222
|
+
* covers a plain UNIQUE index. Checked structurally (message text kept
|
|
1223
|
+
* only as a fallback for runtimes that don't expose `errcode`). */
|
|
1224
|
+
function isUniqueConstraintError(err) {
|
|
1225
|
+
const { errcode } = err;
|
|
1226
|
+
if (errcode !== void 0) return errcode === 1555 || errcode === 2067;
|
|
1227
|
+
return String(err).includes("UNIQUE constraint");
|
|
1228
|
+
}
|
|
1229
|
+
function parseRow(raw) {
|
|
1230
|
+
if (typeof raw !== "string") return null;
|
|
1231
|
+
try {
|
|
1232
|
+
const parsed = JSON.parse(raw);
|
|
1233
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function rowsToItems(rows) {
|
|
1239
|
+
return rows.map((row) => parseRow(row.record)).filter((item) => item !== null);
|
|
1240
|
+
}
|
|
1241
|
+
async function sqliteList(absPath, workspaceRoot) {
|
|
1242
|
+
return withDb(absPath, workspaceRoot, "read", () => [], (database) => rowsToItems(database.prepare("SELECT record FROM records ORDER BY id").all()));
|
|
1243
|
+
}
|
|
1244
|
+
async function sqlitePage(absPath, primaryKey, opts, workspaceRoot) {
|
|
1245
|
+
const emptyPage = {
|
|
1246
|
+
items: [],
|
|
1247
|
+
total: 0,
|
|
1248
|
+
truncated: false
|
|
1249
|
+
};
|
|
1250
|
+
return withDb(absPath, workspaceRoot, "read", () => emptyPage, (database) => {
|
|
1251
|
+
const total = Number(database.prepare("SELECT COUNT(*) AS n FROM records").get().n);
|
|
1252
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
1253
|
+
const limit = opts.limit === void 0 ? -1 : Math.max(0, opts.limit);
|
|
1254
|
+
return {
|
|
1255
|
+
items: projectItemFields(rowsToItems(database.prepare("SELECT record FROM records ORDER BY id LIMIT ? OFFSET ?").all(limit, offset)), opts.fields, primaryKey),
|
|
1256
|
+
total,
|
|
1257
|
+
truncated: false
|
|
1258
|
+
};
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async function sqliteRead(absPath, itemId, workspaceRoot) {
|
|
1262
|
+
const safeId = safeRecordId(itemId);
|
|
1263
|
+
if (safeId === null) return null;
|
|
1264
|
+
return withDb(absPath, workspaceRoot, "read", () => null, (database) => {
|
|
1265
|
+
const row = database.prepare("SELECT record FROM records WHERE id = ?").get(safeId);
|
|
1266
|
+
return row === void 0 ? null : parseRow(row.record);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
async function sqliteWrite(absPath, itemId, item, opts) {
|
|
1270
|
+
const safeId = safeRecordId(itemId);
|
|
1271
|
+
if (safeId === null) return {
|
|
1272
|
+
kind: "invalid-id",
|
|
1273
|
+
itemId
|
|
1274
|
+
};
|
|
1275
|
+
const outcome = await withDb(absPath, opts.workspaceRoot, "write", () => ({
|
|
1276
|
+
kind: "path-escape",
|
|
1277
|
+
itemId: safeId
|
|
1278
|
+
}), (database) => {
|
|
1279
|
+
const payload = JSON.stringify(item);
|
|
1280
|
+
if (opts.refuseOverwrite) try {
|
|
1281
|
+
database.prepare("INSERT INTO records (id, record) VALUES (?, ?)").run(safeId, payload);
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
if (isUniqueConstraintError(err)) return {
|
|
1284
|
+
kind: "conflict",
|
|
1285
|
+
itemId: safeId
|
|
1286
|
+
};
|
|
1287
|
+
throw err;
|
|
1288
|
+
}
|
|
1289
|
+
else database.prepare("INSERT INTO records (id, record) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET record = excluded.record").run(safeId, payload);
|
|
1290
|
+
return {
|
|
1291
|
+
kind: "ok",
|
|
1292
|
+
itemId: safeId,
|
|
1293
|
+
item
|
|
1294
|
+
};
|
|
1295
|
+
});
|
|
1296
|
+
if (outcome.kind === "ok" && opts.slug) publishCollectionChange({
|
|
1297
|
+
slug: opts.slug,
|
|
1298
|
+
ids: [safeId],
|
|
1299
|
+
op: "upsert"
|
|
1300
|
+
});
|
|
1301
|
+
return outcome;
|
|
1302
|
+
}
|
|
1303
|
+
async function sqliteDelete(absPath, itemId, opts) {
|
|
1304
|
+
const safeId = safeRecordId(itemId);
|
|
1305
|
+
if (safeId === null) return {
|
|
1306
|
+
kind: "invalid-id",
|
|
1307
|
+
itemId
|
|
1308
|
+
};
|
|
1309
|
+
const outcome = await withDb(absPath, opts.workspaceRoot, "read", (reason) => reason === "refused" ? {
|
|
1310
|
+
kind: "path-escape",
|
|
1311
|
+
itemId: safeId
|
|
1312
|
+
} : {
|
|
1313
|
+
kind: "not-found",
|
|
1314
|
+
itemId: safeId
|
|
1315
|
+
}, (database) => {
|
|
1316
|
+
const { changes } = database.prepare("DELETE FROM records WHERE id = ?").run(safeId);
|
|
1317
|
+
return Number(changes) === 0 ? {
|
|
1318
|
+
kind: "not-found",
|
|
1319
|
+
itemId: safeId
|
|
1320
|
+
} : {
|
|
1321
|
+
kind: "ok",
|
|
1322
|
+
itemId: safeId
|
|
1323
|
+
};
|
|
1324
|
+
});
|
|
1325
|
+
if (outcome.kind === "ok" && opts.slug) publishCollectionChange({
|
|
1326
|
+
slug: opts.slug,
|
|
1327
|
+
ids: [safeId],
|
|
1328
|
+
op: "delete"
|
|
1329
|
+
});
|
|
1330
|
+
return outcome;
|
|
1331
|
+
}
|
|
1332
|
+
/** Best-effort full WAL checkpoint so the MAIN db file alone is a
|
|
1333
|
+
* complete snapshot (committed pages in `<db>-wal` are folded in and the
|
|
1334
|
+
* WAL truncated). Used by `deleteCollection` before archiving. Returns
|
|
1335
|
+
* false on any failure (runtime without node:sqlite, locked db, missing
|
|
1336
|
+
* file) — the caller then archives the sidecar files alongside the db so
|
|
1337
|
+
* no committed data is lost either way. */
|
|
1338
|
+
async function checkpointSqliteDatabase(absPath) {
|
|
1339
|
+
try {
|
|
1340
|
+
const { DatabaseSync } = await loadSqlite();
|
|
1341
|
+
const database = new DatabaseSync(absPath);
|
|
1342
|
+
try {
|
|
1343
|
+
database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1344
|
+
} finally {
|
|
1345
|
+
database.close();
|
|
1346
|
+
}
|
|
1347
|
+
return true;
|
|
1348
|
+
} catch {
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/** A `storage: sqlite` store over `collection.storageFile`. A schema whose
|
|
1353
|
+
* `storageFile` failed to resolve yields a read-only EMPTY store rather
|
|
1354
|
+
* than a writable one — same fail-closed rule as the CSV store. */
|
|
1355
|
+
function sqliteStoreFor(collection, opts) {
|
|
1356
|
+
const file = collection.storageFile;
|
|
1357
|
+
const key = collection.schema.primaryKey;
|
|
1358
|
+
const slug = opts.slug ?? collection.slug;
|
|
1359
|
+
const root = () => opts.workspaceRoot ?? getWorkspaceRoot();
|
|
1360
|
+
if (file === void 0) return {
|
|
1361
|
+
capabilities: {
|
|
1362
|
+
writable: false,
|
|
1363
|
+
nativeQuery: false,
|
|
1364
|
+
nativePaging: false
|
|
1365
|
+
},
|
|
1366
|
+
list: () => Promise.resolve([]),
|
|
1367
|
+
page: () => Promise.resolve({
|
|
1368
|
+
items: [],
|
|
1369
|
+
total: 0,
|
|
1370
|
+
truncated: false
|
|
1371
|
+
}),
|
|
1372
|
+
read: () => Promise.resolve(null)
|
|
1373
|
+
};
|
|
1374
|
+
return {
|
|
1375
|
+
capabilities: {
|
|
1376
|
+
writable: true,
|
|
1377
|
+
nativeQuery: false,
|
|
1378
|
+
nativePaging: true
|
|
1379
|
+
},
|
|
1380
|
+
list: () => sqliteList(file, root()),
|
|
1381
|
+
page: (pageOpts = {}) => sqlitePage(file, key, pageOpts, root()),
|
|
1382
|
+
read: (itemId) => sqliteRead(file, itemId, root()),
|
|
1383
|
+
write: (itemId, item, writeOpts = {}) => sqliteWrite(file, itemId, item, {
|
|
1384
|
+
workspaceRoot: root(),
|
|
1385
|
+
slug,
|
|
1386
|
+
refuseOverwrite: writeOpts.refuseOverwrite
|
|
1387
|
+
}),
|
|
1388
|
+
delete: (itemId) => sqliteDelete(file, itemId, {
|
|
1389
|
+
workspaceRoot: root(),
|
|
1390
|
+
slug
|
|
1391
|
+
})
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
//#endregion
|
|
1395
|
+
//#region src/collection/server/store.ts
|
|
1396
|
+
/** The file store's stable order: lexicographic by record id (codepoint
|
|
1397
|
+
* compare — locale-independent). `listItems` returns readdir order, which
|
|
1398
|
+
* is filesystem-dependent; paging needs determinism. */
|
|
1399
|
+
function sortByRecordId(items, primaryKey) {
|
|
1400
|
+
return [...items].sort((left, right) => {
|
|
1401
|
+
const leftId = String(left[primaryKey] ?? "");
|
|
1402
|
+
const rightId = String(right[primaryKey] ?? "");
|
|
1403
|
+
if (leftId < rightId) return -1;
|
|
1404
|
+
return leftId > rightId ? 1 : 0;
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
/** True when the collection accepts UI/tool writes. A `dataSource`
|
|
1408
|
+
* collection is read-only: updates happen by editing/replacing the
|
|
1409
|
+
* data file itself. Every write entry point checks this BEFORE calling
|
|
1410
|
+
* `writeItem`/`deleteItem` — server-enforced, not just UI-hidden. */
|
|
1411
|
+
function collectionWritable(collection) {
|
|
1412
|
+
return !require_ids.isReadOnlySchema(collection.schema);
|
|
1413
|
+
}
|
|
1414
|
+
/** The one-line refusal write paths surface (HTTP 405 / MCP error text). */
|
|
1415
|
+
function readOnlyRefusal(slug) {
|
|
1416
|
+
return `collection '${slug}' is read-only (backed by an external dataSource) — update the data file itself instead`;
|
|
1417
|
+
}
|
|
1418
|
+
/** A `dataSource` store over `file` (CSV row order; DuckDB-native query).
|
|
1419
|
+
* A schema whose `dataSourceFile` failed to resolve yields a read-only
|
|
1420
|
+
* EMPTY store rather than falling back to the (writable) file store — a
|
|
1421
|
+
* half-loaded read-only collection must never become writable. */
|
|
1422
|
+
function csvStoreFor(collection, opts) {
|
|
1423
|
+
const file = collection.dataSourceFile;
|
|
1424
|
+
const key = collection.schema.primaryKey;
|
|
1425
|
+
const listAll = () => file === void 0 ? Promise.resolve({
|
|
1426
|
+
items: [],
|
|
1427
|
+
truncated: false
|
|
1428
|
+
}) : csvList(file, key, opts.workspaceRoot);
|
|
1429
|
+
return {
|
|
1430
|
+
capabilities: {
|
|
1431
|
+
writable: false,
|
|
1432
|
+
nativeQuery: true,
|
|
1433
|
+
nativePaging: false
|
|
1434
|
+
},
|
|
1435
|
+
list: () => listAll().then((result) => result.items),
|
|
1436
|
+
page: (pageOpts = {}) => listAll().then((result) => pageFromFullRead(result.items, pageOpts, key, result.truncated)),
|
|
1437
|
+
read: (itemId) => file === void 0 ? Promise.resolve(null) : csvRead(file, key, itemId, opts.workspaceRoot),
|
|
1438
|
+
query: (query) => file === void 0 ? Promise.resolve([]) : csvRunQuery(file, key, query, opts.workspaceRoot)
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
/** The classic file store over `<dataDir>/<itemId>.json` records. */
|
|
1442
|
+
function fileStoreFor(collection, opts) {
|
|
1443
|
+
const key = collection.schema.primaryKey;
|
|
1444
|
+
const ioOpts = {
|
|
1445
|
+
...opts,
|
|
1446
|
+
slug: opts.slug ?? collection.slug
|
|
1447
|
+
};
|
|
1448
|
+
return {
|
|
1449
|
+
capabilities: {
|
|
1450
|
+
writable: true,
|
|
1451
|
+
nativeQuery: false,
|
|
1452
|
+
nativePaging: false
|
|
1453
|
+
},
|
|
1454
|
+
list: () => listItems(collection.dataDir, opts),
|
|
1455
|
+
page: async (pageOpts = {}) => pageFromFullRead(sortByRecordId(await listItems(collection.dataDir, opts), key), pageOpts, key, false),
|
|
1456
|
+
read: (itemId) => readItem(collection.dataDir, itemId, opts),
|
|
1457
|
+
write: (itemId, item, writeOpts = {}) => writeItem(collection.dataDir, itemId, item, {
|
|
1458
|
+
...ioOpts,
|
|
1459
|
+
refuseOverwrite: writeOpts.refuseOverwrite
|
|
1460
|
+
}),
|
|
1461
|
+
delete: (itemId) => deleteItem(collection.dataDir, itemId, ioOpts)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
var storeFactories = /* @__PURE__ */ new Map([
|
|
1465
|
+
["file", fileStoreFor],
|
|
1466
|
+
["csv", csvStoreFor],
|
|
1467
|
+
["sqlite", sqliteStoreFor]
|
|
1468
|
+
]);
|
|
1469
|
+
/** Pick the store implementation for a discovered collection via the
|
|
1470
|
+
* factory registry. An unknown kind cannot normally reach here (the
|
|
1471
|
+
* schema's `StorageZ` union gates it), so the throw is a loud invariant
|
|
1472
|
+
* breach, not a user-facing path. */
|
|
1473
|
+
function storeFor(collection, opts = {}) {
|
|
1474
|
+
const kind = require_ids.storageKindFor(collection.schema);
|
|
1475
|
+
const factory = storeFactories.get(kind);
|
|
1476
|
+
if (!factory) throw new Error(`no store factory registered for storage kind '${kind}'`);
|
|
1477
|
+
return factory(collection, opts);
|
|
1478
|
+
}
|
|
638
1479
|
/** The param name a `set` value references, or null when the value is a
|
|
639
1480
|
* literal (non-strings can never be references). A bare/empty prefix
|
|
640
1481
|
* (`"$params."`) returns the empty string — the schema refine rejects
|
|
@@ -1210,11 +2051,24 @@ var DataSourceZ = zod.z.object({
|
|
|
1210
2051
|
type: zod.z.literal("csv"),
|
|
1211
2052
|
path: zod.z.string().min(1)
|
|
1212
2053
|
});
|
|
2054
|
+
/** Alternative WRITABLE storage backend for a collection's records —
|
|
2055
|
+
* unlike `dataSource` (external read-only file), a `storage` collection
|
|
2056
|
+
* behaves like a normal writable collection; only where the rows live
|
|
2057
|
+
* changes. v1: `sqlite` — records in a single SQLite database file
|
|
2058
|
+
* (`node:sqlite`, one JSON record per row keyed by the primaryKey).
|
|
2059
|
+
* `path` is workspace-relative and containment-checked exactly like
|
|
2060
|
+
* `dataPath`. The store factory registry (`server/store.ts`) picks the
|
|
2061
|
+
* implementation by `type` (plans/refactor-storage-virtualization.md). */
|
|
2062
|
+
var StorageZ = zod.z.object({
|
|
2063
|
+
type: zod.z.literal("sqlite"),
|
|
2064
|
+
path: zod.z.string().min(1)
|
|
2065
|
+
});
|
|
1213
2066
|
var BareCollectionSchemaZ = zod.z.object({
|
|
1214
2067
|
title: zod.z.string().min(1),
|
|
1215
2068
|
icon: zod.z.string().min(1),
|
|
1216
2069
|
dataPath: zod.z.string().min(1).optional(),
|
|
1217
2070
|
dataSource: DataSourceZ.optional(),
|
|
2071
|
+
storage: StorageZ.optional(),
|
|
1218
2072
|
primaryKey: zod.z.string().min(1),
|
|
1219
2073
|
singleton: zod.z.string().trim().min(1).optional(),
|
|
1220
2074
|
fields: zod.z.record(zod.z.string(), FieldSpecZ),
|
|
@@ -1235,8 +2089,12 @@ var BareCollectionSchemaZ = zod.z.object({
|
|
|
1235
2089
|
ingest: IngestZ.optional(),
|
|
1236
2090
|
googleCalendar: GoogleCalendarSyncZ.optional(),
|
|
1237
2091
|
dynamicIcon: DynamicIconSpecZ.optional()
|
|
1238
|
-
}).refine((schema) =>
|
|
1239
|
-
|
|
2092
|
+
}).refine((schema) => [
|
|
2093
|
+
schema.dataPath,
|
|
2094
|
+
schema.dataSource,
|
|
2095
|
+
schema.storage
|
|
2096
|
+
].filter((declared) => declared !== void 0).length === 1, {
|
|
2097
|
+
message: "declare exactly one of `dataPath` (native JSON records), `dataSource` (external read-only data file), or `storage` (alternative writable backend)",
|
|
1240
2098
|
path: ["dataPath"]
|
|
1241
2099
|
}).refine((schema) => schema.dataSource === void 0 || schema.singleton === void 0 && schema.ingest === void 0 && schema.spawn === void 0 && schema.googleCalendar === void 0, {
|
|
1242
2100
|
message: "a `dataSource` collection is read-only — it cannot declare `singleton`, `ingest`, `spawn`, or `googleCalendar` (all of them write records)",
|
|
@@ -1487,6 +2345,23 @@ function acceptParsedSchema(schema, opts) {
|
|
|
1487
2345
|
dataSourceFile
|
|
1488
2346
|
};
|
|
1489
2347
|
}
|
|
2348
|
+
if (schema.storage !== void 0) {
|
|
2349
|
+
const storageFile = resolveDataDir(schema.storage.path, opts.workspaceRoot);
|
|
2350
|
+
if (storageFile === null) return {
|
|
2351
|
+
ok: false,
|
|
2352
|
+
reason: `storage.path '${schema.storage.path}' escapes the workspace`
|
|
2353
|
+
};
|
|
2354
|
+
const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
|
|
2355
|
+
if (dataDir === null) return {
|
|
2356
|
+
ok: false,
|
|
2357
|
+
reason: `slug '${opts.slug}' yields no workspace-contained data dir`
|
|
2358
|
+
};
|
|
2359
|
+
return {
|
|
2360
|
+
ok: true,
|
|
2361
|
+
dataDir,
|
|
2362
|
+
storageFile
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
1490
2365
|
const dataDir = resolveDataDir(schema.dataPath ?? "", opts.workspaceRoot);
|
|
1491
2366
|
if (dataDir === null) return {
|
|
1492
2367
|
ok: false,
|
|
@@ -1551,6 +2426,7 @@ async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
|
|
|
1551
2426
|
schema,
|
|
1552
2427
|
dataDir: acceptance.dataDir,
|
|
1553
2428
|
...acceptance.dataSourceFile !== void 0 ? { dataSourceFile: acceptance.dataSourceFile } : {},
|
|
2429
|
+
...acceptance.storageFile !== void 0 ? { storageFile: acceptance.storageFile } : {},
|
|
1554
2430
|
skillDir: node_path.default.join(skillsRoot, safeName)
|
|
1555
2431
|
};
|
|
1556
2432
|
}
|
|
@@ -1633,12 +2509,36 @@ function toDetail(collection) {
|
|
|
1633
2509
|
};
|
|
1634
2510
|
}
|
|
1635
2511
|
//#endregion
|
|
2512
|
+
Object.defineProperty(exports, "CollectionQueryZ", {
|
|
2513
|
+
enumerable: true,
|
|
2514
|
+
get: function() {
|
|
2515
|
+
return CollectionQueryZ;
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
1636
2518
|
Object.defineProperty(exports, "CollectionSchemaZ", {
|
|
1637
2519
|
enumerable: true,
|
|
1638
2520
|
get: function() {
|
|
1639
2521
|
return CollectionSchemaZ;
|
|
1640
2522
|
}
|
|
1641
2523
|
});
|
|
2524
|
+
Object.defineProperty(exports, "DEFAULT_QUERY_ROWS", {
|
|
2525
|
+
enumerable: true,
|
|
2526
|
+
get: function() {
|
|
2527
|
+
return DEFAULT_QUERY_ROWS;
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
Object.defineProperty(exports, "MAX_CSV_ROWS", {
|
|
2531
|
+
enumerable: true,
|
|
2532
|
+
get: function() {
|
|
2533
|
+
return MAX_CSV_ROWS;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
Object.defineProperty(exports, "MAX_QUERY_ROWS", {
|
|
2537
|
+
enumerable: true,
|
|
2538
|
+
get: function() {
|
|
2539
|
+
return MAX_QUERY_ROWS;
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
1642
2542
|
Object.defineProperty(exports, "SCHEMA_FILE", {
|
|
1643
2543
|
enumerable: true,
|
|
1644
2544
|
get: function() {
|
|
@@ -1669,18 +2569,66 @@ Object.defineProperty(exports, "buildCollectionActionSeedPrompt", {
|
|
|
1669
2569
|
return buildCollectionActionSeedPrompt;
|
|
1670
2570
|
}
|
|
1671
2571
|
});
|
|
2572
|
+
Object.defineProperty(exports, "cacheDir", {
|
|
2573
|
+
enumerable: true,
|
|
2574
|
+
get: function() {
|
|
2575
|
+
return cacheDir;
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
Object.defineProperty(exports, "checkpointSqliteDatabase", {
|
|
2579
|
+
enumerable: true,
|
|
2580
|
+
get: function() {
|
|
2581
|
+
return checkpointSqliteDatabase;
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
Object.defineProperty(exports, "collectionWritable", {
|
|
2585
|
+
enumerable: true,
|
|
2586
|
+
get: function() {
|
|
2587
|
+
return collectionWritable;
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
1672
2590
|
Object.defineProperty(exports, "collectionsRegistriesConfigPath", {
|
|
1673
2591
|
enumerable: true,
|
|
1674
2592
|
get: function() {
|
|
1675
2593
|
return collectionsRegistriesConfigPath;
|
|
1676
2594
|
}
|
|
1677
2595
|
});
|
|
2596
|
+
Object.defineProperty(exports, "compileCsvQuery", {
|
|
2597
|
+
enumerable: true,
|
|
2598
|
+
get: function() {
|
|
2599
|
+
return compileCsvQuery;
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
Object.defineProperty(exports, "compileJsonlQuery", {
|
|
2603
|
+
enumerable: true,
|
|
2604
|
+
get: function() {
|
|
2605
|
+
return compileJsonlQuery;
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
1678
2608
|
Object.defineProperty(exports, "configureCollectionHost", {
|
|
1679
2609
|
enumerable: true,
|
|
1680
2610
|
get: function() {
|
|
1681
2611
|
return configureCollectionHost;
|
|
1682
2612
|
}
|
|
1683
2613
|
});
|
|
2614
|
+
Object.defineProperty(exports, "csvRowToItem", {
|
|
2615
|
+
enumerable: true,
|
|
2616
|
+
get: function() {
|
|
2617
|
+
return csvRowToItem;
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
Object.defineProperty(exports, "decodeCsvRecordId", {
|
|
2621
|
+
enumerable: true,
|
|
2622
|
+
get: function() {
|
|
2623
|
+
return decodeCsvRecordId;
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2626
|
+
Object.defineProperty(exports, "dedupeByRecordId", {
|
|
2627
|
+
enumerable: true,
|
|
2628
|
+
get: function() {
|
|
2629
|
+
return dedupeByRecordId;
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
1684
2632
|
Object.defineProperty(exports, "deleteItem", {
|
|
1685
2633
|
enumerable: true,
|
|
1686
2634
|
get: function() {
|
|
@@ -1693,6 +2641,12 @@ Object.defineProperty(exports, "discoverCollections", {
|
|
|
1693
2641
|
return discoverCollections;
|
|
1694
2642
|
}
|
|
1695
2643
|
});
|
|
2644
|
+
Object.defineProperty(exports, "encodeCsvRecordId", {
|
|
2645
|
+
enumerable: true,
|
|
2646
|
+
get: function() {
|
|
2647
|
+
return encodeCsvRecordId;
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
1696
2650
|
Object.defineProperty(exports, "generateItemId", {
|
|
1697
2651
|
enumerable: true,
|
|
1698
2652
|
get: function() {
|
|
@@ -1753,6 +2707,24 @@ Object.defineProperty(exports, "log", {
|
|
|
1753
2707
|
return log;
|
|
1754
2708
|
}
|
|
1755
2709
|
});
|
|
2710
|
+
Object.defineProperty(exports, "normalizeCsvValue", {
|
|
2711
|
+
enumerable: true,
|
|
2712
|
+
get: function() {
|
|
2713
|
+
return normalizeCsvValue;
|
|
2714
|
+
}
|
|
2715
|
+
});
|
|
2716
|
+
Object.defineProperty(exports, "pageFromFullRead", {
|
|
2717
|
+
enumerable: true,
|
|
2718
|
+
get: function() {
|
|
2719
|
+
return pageFromFullRead;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
Object.defineProperty(exports, "projectItemFields", {
|
|
2723
|
+
enumerable: true,
|
|
2724
|
+
get: function() {
|
|
2725
|
+
return projectItemFields;
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
1756
2728
|
Object.defineProperty(exports, "promptPathsFor", {
|
|
1757
2729
|
enumerable: true,
|
|
1758
2730
|
get: function() {
|
|
@@ -1765,6 +2737,12 @@ Object.defineProperty(exports, "publishCollectionChange", {
|
|
|
1765
2737
|
return publishCollectionChange;
|
|
1766
2738
|
}
|
|
1767
2739
|
});
|
|
2740
|
+
Object.defineProperty(exports, "queryCsv", {
|
|
2741
|
+
enumerable: true,
|
|
2742
|
+
get: function() {
|
|
2743
|
+
return queryCsv;
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
1768
2746
|
Object.defineProperty(exports, "readCustomViewHtml", {
|
|
1769
2747
|
enumerable: true,
|
|
1770
2748
|
get: function() {
|
|
@@ -1783,6 +2761,12 @@ Object.defineProperty(exports, "readItem", {
|
|
|
1783
2761
|
return readItem;
|
|
1784
2762
|
}
|
|
1785
2763
|
});
|
|
2764
|
+
Object.defineProperty(exports, "readOnlyRefusal", {
|
|
2765
|
+
enumerable: true,
|
|
2766
|
+
get: function() {
|
|
2767
|
+
return readOnlyRefusal;
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
1786
2770
|
Object.defineProperty(exports, "readSkillTemplate", {
|
|
1787
2771
|
enumerable: true,
|
|
1788
2772
|
get: function() {
|
|
@@ -1837,6 +2821,12 @@ Object.defineProperty(exports, "skillsStagingDir", {
|
|
|
1837
2821
|
return skillsStagingDir;
|
|
1838
2822
|
}
|
|
1839
2823
|
});
|
|
2824
|
+
Object.defineProperty(exports, "storeFor", {
|
|
2825
|
+
enumerable: true,
|
|
2826
|
+
get: function() {
|
|
2827
|
+
return storeFor;
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
1840
2830
|
Object.defineProperty(exports, "toDetail", {
|
|
1841
2831
|
enumerable: true,
|
|
1842
2832
|
get: function() {
|
|
@@ -1862,4 +2852,4 @@ Object.defineProperty(exports, "writeItem", {
|
|
|
1862
2852
|
}
|
|
1863
2853
|
});
|
|
1864
2854
|
|
|
1865
|
-
//# sourceMappingURL=discovery-
|
|
2855
|
+
//# sourceMappingURL=discovery-cwtfyDxl.cjs.map
|