@bobbykim/manguito-cms-cli 0.10.1 → 0.11.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.
@@ -9270,7 +9270,7 @@ var require_bcryptjs = __commonJS({
9270
9270
  }
9271
9271
  });
9272
9272
 
9273
- // ../api/dist/chunk-IHEIX6P4.mjs
9273
+ // ../api/dist/chunk-GUZZQZSN.mjs
9274
9274
  init_esm_shims();
9275
9275
  import crypto from "crypto";
9276
9276
 
@@ -25795,7 +25795,8 @@ var fieldTypeRegistry = {
25795
25795
  left_column: "left_id",
25796
25796
  right_column: "right_id",
25797
25797
  right_table: targetTableName,
25798
- order_column: false
25798
+ // Every list keeps the order the editor gave it (#57).
25799
+ order_column: true
25799
25800
  }
25800
25801
  } : {
25801
25802
  // FK column on the owning table. An optional reference is cleared when
@@ -26617,7 +26618,7 @@ function describeSchemaChange(input) {
26617
26618
  return { from: from?.version ?? null, to: to.version, types, identical };
26618
26619
  }
26619
26620
 
26620
- // ../api/dist/chunk-IHEIX6P4.mjs
26621
+ // ../api/dist/chunk-GUZZQZSN.mjs
26621
26622
  function isColumnBacked2(field) {
26622
26623
  const col = field.db_column;
26623
26624
  if (col === null) return false;
@@ -26845,10 +26846,28 @@ async function persistJunctionField(db, itemId, junction, relatedIds) {
26845
26846
  const tbl = sql.raw(quoteIdent(junction.table_name));
26846
26847
  const left = sql.raw(quoteIdent(junction.left_column));
26847
26848
  const right = sql.raw(quoteIdent(junction.right_column));
26849
+ const seen = /* @__PURE__ */ new Set();
26850
+ const ids = relatedIds.filter((id) => {
26851
+ const key = id.toLowerCase();
26852
+ if (seen.has(key)) return false;
26853
+ seen.add(key);
26854
+ return true;
26855
+ });
26848
26856
  await db.execute(sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
26849
- for (const rid of relatedIds) {
26850
- await db.execute(sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
26851
- }
26857
+ if (ids.length === 0) return;
26858
+ const rows = sql.join(
26859
+ ids.map(
26860
+ (rid, position) => junction.order_column ? sql`(${itemId}, ${rid}, ${position})` : sql`(${itemId}, ${rid})`
26861
+ ),
26862
+ sql`, `
26863
+ );
26864
+ await db.execute(
26865
+ junction.order_column ? sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows}` : sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`
26866
+ );
26867
+ }
26868
+ function junctionOrderBy(junction) {
26869
+ const right = sql.raw(quoteIdent(junction.right_column));
26870
+ return junction.order_column ? sql` ORDER BY "order" ASC, ${right} ASC` : sql` ORDER BY ${right} ASC`;
26852
26871
  }
26853
26872
  function oneOrMany(cardinality, list) {
26854
26873
  return cardinality === "one" ? list[0] ?? null : list;
@@ -26950,9 +26969,8 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26950
26969
  } else if (rel.type === "junction") {
26951
26970
  const parentIds = batch.map((r) => r["id"]);
26952
26971
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
26953
- const orderBy = rel.order_column ? sql` ORDER BY "order" ASC` : sql``;
26954
26972
  const junctionResult = await db.execute(
26955
- sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
26973
+ sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
26956
26974
  );
26957
26975
  const jRows = junctionResult.rows;
26958
26976
  const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
@@ -27011,7 +27029,7 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
27011
27029
  const parentIds = rows.map((r) => r["id"]);
27012
27030
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
27013
27031
  const result = await db.execute(
27014
- sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
27032
+ sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
27015
27033
  );
27016
27034
  const byLeft = groupBy(result.rows, "left_id");
27017
27035
  for (const row of rows) {
@@ -27071,6 +27089,7 @@ export {
27071
27089
  persistParagraphField,
27072
27090
  deleteParagraphField,
27073
27091
  persistJunctionField,
27092
+ junctionOrderBy,
27074
27093
  oneOrMany,
27075
27094
  paragraphItems,
27076
27095
  buildRelationsMap,
@@ -12,7 +12,7 @@ import {
12
12
  isColumnBacked,
13
13
  relationCardinality,
14
14
  resolveRelationField
15
- } from "./chunk-X7D2QPPO.js";
15
+ } from "./chunk-WTZBHFCV.js";
16
16
 
17
17
  // ../../node_modules/.pnpm/@whatwg-node+fetch@0.10.13/node_modules/@whatwg-node/fetch/dist/shouldSkipPonyfill.js
18
18
  var require_shouldSkipPonyfill = __commonJS({
@@ -10787,10 +10787,10 @@ var require_parser = __commonJS({
10787
10787
  }
10788
10788
  });
10789
10789
 
10790
- // ../api/dist/handler-VEOWJRAL.mjs
10790
+ // ../api/dist/handler-MFF32JB4.mjs
10791
10791
  init_esm_shims();
10792
10792
 
10793
- // ../api/dist/chunk-Y5SWT7PO.mjs
10793
+ // ../api/dist/chunk-ITOSMGDT.mjs
10794
10794
  init_esm_shims();
10795
10795
 
10796
10796
  // ../../node_modules/.pnpm/graphql-yoga@5.21.2_graphql@16.14.2/node_modules/graphql-yoga/esm/index.js
@@ -30123,7 +30123,7 @@ function createYoga(options) {
30123
30123
  });
30124
30124
  }
30125
30125
 
30126
- // ../api/dist/chunk-Y5SWT7PO.mjs
30126
+ // ../api/dist/chunk-ITOSMGDT.mjs
30127
30127
  var import_dataloader = __toESM(require_dataloader(), 1);
30128
30128
 
30129
30129
  // ../../node_modules/.pnpm/@escape.tech+graphql-armor@3.2.0_@envelop+core@5.5.1_@escape.tech+graphql-armor-types@0.7.0/node_modules/@escape.tech/graphql-armor/dist/graphql-armor.esm.js
@@ -30881,7 +30881,7 @@ var EnvelopArmor = class {
30881
30881
  }
30882
30882
  };
30883
30883
 
30884
- // ../api/dist/chunk-Y5SWT7PO.mjs
30884
+ // ../api/dist/chunk-ITOSMGDT.mjs
30885
30885
  var DateTimeScalar = new GraphQLScalarType({
30886
30886
  name: "DateTime",
30887
30887
  description: "ISO-8601 date-time string",
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  isColumnBacked,
38
38
  isDriverValueEncoder,
39
39
  isSQLWrapper,
40
+ junctionOrderBy,
40
41
  loadSchemaFile,
41
42
  loadVersionSnapshots,
42
43
  oneOrMany,
@@ -54,7 +55,7 @@ import {
54
55
  validateCrossReferences,
55
56
  verifyPassword,
56
57
  walkSchemaDirectory
57
- } from "./chunk-X7D2QPPO.js";
58
+ } from "./chunk-WTZBHFCV.js";
58
59
 
59
60
  // ../../node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js
60
61
  var require_postgres_array = __commonJS({
@@ -22848,8 +22849,7 @@ function checkRelationInput(fields, body, registry, path5 = "") {
22848
22849
  });
22849
22850
  continue;
22850
22851
  }
22851
- const comp = field.ui_component;
22852
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
22852
+ const pType = paragraphTypeOf(field, registry);
22853
22853
  if (!pType) continue;
22854
22854
  items.forEach((item, i) => {
22855
22855
  const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
@@ -22858,12 +22858,15 @@ function checkRelationInput(fields, body, registry, path5 = "") {
22858
22858
  }
22859
22859
  return errors;
22860
22860
  }
22861
+ function paragraphTypeOf(field, registry) {
22862
+ const comp = field.ui_component;
22863
+ return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
22864
+ }
22861
22865
  function paragraphItemsIn(fields, body, registry, path5) {
22862
22866
  const out = [];
22863
22867
  for (const field of fields) {
22864
22868
  if (field.field_type !== "paragraph" || !(field.name in body)) continue;
22865
- const comp = field.ui_component;
22866
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
22869
+ const pType = paragraphTypeOf(field, registry);
22867
22870
  if (!pType) continue;
22868
22871
  const value = body[field.name];
22869
22872
  const name = `${path5}${field.name}`;
@@ -22934,6 +22937,131 @@ async function findMissingReferences(db, fields, body, registry) {
22934
22937
  const found = new Map(await Promise.all(lookups));
22935
22938
  return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
22936
22939
  }
22940
+ var VARCHAR_DEFAULT_LIMIT = 255;
22941
+ var INTEGER_MIN = -2147483648;
22942
+ var INTEGER_MAX = 2147483647;
22943
+ var DECIMAL_LIMIT = 1e6;
22944
+ var ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
22945
+ var DECIMAL_STRING = /^-?\d+(?:\.\d+)?$/;
22946
+ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enum"]);
22947
+ function emptyValuesToNull(fields, body, registry) {
22948
+ const out = { ...body };
22949
+ for (const f of fields) {
22950
+ if (!(f.name in out)) continue;
22951
+ const value = out[f.name];
22952
+ if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
22953
+ out[f.name] = null;
22954
+ continue;
22955
+ }
22956
+ if (f.field_type !== "paragraph") continue;
22957
+ const pType = paragraphTypeOf(f, registry);
22958
+ if (!pType) continue;
22959
+ if (isPlainObject(value)) out[f.name] = emptyValuesToNull(pType.fields, value, registry);
22960
+ else if (Array.isArray(value)) {
22961
+ out[f.name] = value.map((item) => isPlainObject(item) ? emptyValuesToNull(pType.fields, item, registry) : item);
22962
+ }
22963
+ }
22964
+ return out;
22965
+ }
22966
+ var compiled = /* @__PURE__ */ new Map();
22967
+ function patternOf(source) {
22968
+ if (!compiled.has(source)) {
22969
+ let re;
22970
+ try {
22971
+ re = new RegExp(source);
22972
+ } catch {
22973
+ re = null;
22974
+ }
22975
+ compiled.set(source, re);
22976
+ }
22977
+ return compiled.get(source) ?? null;
22978
+ }
22979
+ function isRealDate(value) {
22980
+ const m2 = ISO_TIMESTAMP.exec(value);
22981
+ if (!m2) return false;
22982
+ const [y, mo, d2, h = 0, mi2 = 0, sec = 0] = m2.slice(1).map((part) => part === void 0 ? void 0 : Number(part));
22983
+ if (y === void 0 || mo === void 0 || d2 === void 0 || y < 1) return false;
22984
+ const date2 = new Date(Date.UTC(y, mo - 1, d2));
22985
+ date2.setUTCFullYear(y);
22986
+ return date2.getUTCMonth() === mo - 1 && date2.getUTCDate() === d2 && h <= 23 && mi2 <= 59 && sec <= 59;
22987
+ }
22988
+ function valueError(f, value, name) {
22989
+ const v2 = f.validation;
22990
+ if (Array.isArray(value) && (f.field_type === "paragraph" || f.field_type === "reference")) {
22991
+ if (v2.max_items !== void 0 && value.length > v2.max_items) {
22992
+ return `${name} can hold at most ${v2.max_items} items.`;
22993
+ }
22994
+ return null;
22995
+ }
22996
+ const col = f.db_column;
22997
+ if (!col || col.junction || col.column_type === "uuid") return null;
22998
+ if (value === null || value === void 0) {
22999
+ if (col.nullable) return null;
23000
+ return f.required ? `${name} is required.` : `${name} cannot be empty.`;
23001
+ }
23002
+ switch (f.field_type) {
23003
+ case "text/plain": {
23004
+ if (typeof value !== "string") return `${name} must be text.`;
23005
+ if (value.includes("\0")) return `${name} contains a character that cannot be stored (U+0000).`;
23006
+ const limit = v2.limit ?? VARCHAR_DEFAULT_LIMIT;
23007
+ if ([...value].length > limit) return `${name} can be at most ${limit} characters.`;
23008
+ const pattern = v2.pattern === void 0 ? null : patternOf(v2.pattern);
23009
+ if (value !== "" && pattern && !pattern.test(value)) return `${name} is not in the correct format.`;
23010
+ return null;
23011
+ }
23012
+ case "text/rich":
23013
+ if (typeof value !== "string") return `${name} must be text.`;
23014
+ return value.includes("\0") ? `${name} contains a character that cannot be stored (U+0000).` : null;
23015
+ case "integer":
23016
+ if (typeof value !== "number" || !Number.isInteger(value)) return `${name} must be a whole number.`;
23017
+ if (value < INTEGER_MIN || value > INTEGER_MAX) {
23018
+ return `${name} must be between ${INTEGER_MIN} and ${INTEGER_MAX}.`;
23019
+ }
23020
+ return rangeError(name, value, v2.min, v2.max);
23021
+ case "float": {
23022
+ const n = typeof value === "number" ? value : typeof value === "string" && DECIMAL_STRING.test(value) ? Number(value) : NaN;
23023
+ if (!Number.isFinite(n)) return `${name} must be a number.`;
23024
+ if (roundedSize(typeof value === "string" ? value : String(n)) >= DECIMAL_LIMIT) {
23025
+ return `${name} must be less than ${DECIMAL_LIMIT} in size.`;
23026
+ }
23027
+ return rangeError(name, n, v2.min, v2.max);
23028
+ }
23029
+ case "boolean":
23030
+ return typeof value === "boolean" ? null : `${name} must be true or false.`;
23031
+ case "date":
23032
+ return typeof value === "string" && isRealDate(value) ? null : `${name} must be a date, such as 2026-10-11 or 2026-10-11T09:30:00Z.`;
23033
+ case "enum": {
23034
+ const allowed = v2.allowed_values ?? [];
23035
+ return typeof value === "string" && allowed.includes(value) ? null : `${name} must be one of: ${allowed.join(", ")}.`;
23036
+ }
23037
+ default:
23038
+ return null;
23039
+ }
23040
+ }
23041
+ function roundedSize(text2) {
23042
+ if (text2.includes("e")) return Math.abs(Number(text2));
23043
+ const [int = "0", frac = ""] = text2.replace("-", "").split(".");
23044
+ const kept = Number(`${int}.${frac.slice(0, 4) || "0"}`);
23045
+ return frac.length > 4 && frac.charAt(4) >= "5" ? kept + 1e-4 : kept;
23046
+ }
23047
+ function rangeError(name, value, min, max) {
23048
+ if (min !== void 0 && value < min) return `${name} must be at least ${min}.`;
23049
+ if (max !== void 0 && value > max) return `${name} must be at most ${max}.`;
23050
+ return null;
23051
+ }
23052
+ function checkFieldValues(fields, body, registry, path5 = "") {
23053
+ const errors = [];
23054
+ for (const f of fields) {
23055
+ if (!(f.name in body) || f.removed === true) continue;
23056
+ const name = `${path5}${f.name}`;
23057
+ const message = valueError(f, body[f.name], name);
23058
+ if (message) errors.push({ field: name, message });
23059
+ }
23060
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path5)) {
23061
+ errors.push(...checkFieldValues(itemFields, item, registry, prefix));
23062
+ }
23063
+ return errors;
23064
+ }
22937
23065
  function quoteIdent3(name) {
22938
23066
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
22939
23067
  return `"${name}"`;
@@ -22969,7 +23097,7 @@ async function loadRelationsForEdit(db, registry, fields, id, row) {
22969
23097
  } else if (f.db_column.junction) {
22970
23098
  const j = f.db_column.junction;
22971
23099
  const r = await db.execute(
22972
- sql`SELECT ${sql.raw(quoteIdent3(j.right_column))} FROM ${sql.raw(quoteIdent3(j.table_name))} WHERE ${sql.raw(quoteIdent3(j.left_column))} = ${id}`
23100
+ sql`SELECT ${sql.raw(quoteIdent3(j.right_column))} FROM ${sql.raw(quoteIdent3(j.table_name))} WHERE ${sql.raw(quoteIdent3(j.left_column))} = ${id}${junctionOrderBy(j)}`
22973
23101
  );
22974
23102
  row[f.name] = r.rows.map((linked) => linked[j.right_column]);
22975
23103
  }
@@ -23012,6 +23140,8 @@ async function relationInputError(c, fields, body, registry, db) {
23012
23140
  const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
23013
23141
  const shape = checkRelationInput(fields, body, registry);
23014
23142
  if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
23143
+ const values = checkFieldValues(fields, body, registry);
23144
+ if (values.length > 0) return refuse("Some values are not valid", values);
23015
23145
  const required = checkRequiredInParagraphItems(fields, body, registry);
23016
23146
  if (required.length > 0) return refuse("Required fields are missing", required);
23017
23147
  if (db) {
@@ -23162,7 +23292,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23162
23292
  `/admin/api/${basePath}`,
23163
23293
  requirePermission("content:create"),
23164
23294
  async (c) => {
23165
- const body = await c.req.json();
23295
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
23166
23296
  if (contentType.only_one) {
23167
23297
  const existing = await repo.findMany({ page: 1, per_page: 1 });
23168
23298
  if (existing.meta.total > 0) {
@@ -23289,7 +23419,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23289
23419
  requirePermission("content:edit"),
23290
23420
  async (c) => {
23291
23421
  const id = c.req.param("id");
23292
- const body = await c.req.json();
23422
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
23293
23423
  const existing = await repo.findOne(id);
23294
23424
  if (!existing) {
23295
23425
  return c.json(
@@ -23396,7 +23526,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23396
23526
  `/admin/api/${basePath}`,
23397
23527
  requirePermission("content:edit"),
23398
23528
  async (c) => {
23399
- const body = await c.req.json();
23529
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
23400
23530
  const existingResult = await repo.findMany({ page: 1, per_page: 1 });
23401
23531
  const existing = existingResult.data[0];
23402
23532
  if (!existing) {
@@ -23523,7 +23653,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23523
23653
  `/admin/api/taxonomy/${typeName}`,
23524
23654
  requirePermission("content:create"),
23525
23655
  async (c) => {
23526
- const body = await c.req.json();
23656
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
23527
23657
  const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
23528
23658
  if (inputError) return inputError;
23529
23659
  const storageBody = fieldKeys.toStorage(body);
@@ -23571,7 +23701,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23571
23701
  requirePermission("content:edit"),
23572
23702
  async (c) => {
23573
23703
  const id = c.req.param("id");
23574
- const body = await c.req.json();
23704
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
23575
23705
  const storageBody = fieldKeys.toStorage(body);
23576
23706
  const existing = await repo.findOne(id);
23577
23707
  if (!existing) {
@@ -24609,7 +24739,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
24609
24739
  }
24610
24740
  var API_VERSION = (() => {
24611
24741
  try {
24612
- return "0.8.4";
24742
+ return "0.9.0";
24613
24743
  } catch {
24614
24744
  return "0.0.0";
24615
24745
  }
@@ -25292,7 +25422,7 @@ function createCmsApp(options) {
25292
25422
  handler: null,
25293
25423
  error: null
25294
25424
  }));
25295
- const ready = import("./handler-VEOWJRAL-OU66YS5U.js").then(({ createGraphQLHandler }) => {
25425
+ const ready = import("./handler-MFF32JB4-MEMKPHHQ.js").then(({ createGraphQLHandler }) => {
25296
25426
  gqlPasses.forEach((pass, i) => {
25297
25427
  const mount = mounts[i];
25298
25428
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "author": "Bobby Kim <bobby.sihun.kim@gmail.com>",
6
6
  "homepage": "https://github.com/bobbykim89/manguito-cms",
@@ -29,10 +29,10 @@
29
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^6.0.3",
31
31
  "vite": "^8.1.3",
32
- "@bobbykim/manguito-cms-admin": "0.5.3",
33
- "@bobbykim/manguito-cms-api": "0.8.4",
34
- "@bobbykim/manguito-cms-db": "0.1.9",
35
- "@bobbykim/manguito-cms-core": "0.9.0"
32
+ "@bobbykim/manguito-cms-admin": "0.5.4",
33
+ "@bobbykim/manguito-cms-api": "0.9.0",
34
+ "@bobbykim/manguito-cms-core": "0.10.0",
35
+ "@bobbykim/manguito-cms-db": "0.1.10"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",