@objectstack/metadata 17.0.0-rc.4 → 17.0.0-rc.6

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/dist/node.js CHANGED
@@ -516,6 +516,7 @@ var LRUCache = class {
516
516
  };
517
517
 
518
518
  // src/utils/schema-sync-errors.ts
519
+ import { isRelationSubObjectPhrase } from "@objectstack/types";
519
520
  var ALREADY_EXISTS = {
520
521
  codes: /* @__PURE__ */ new Set([
521
522
  // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
@@ -559,14 +560,67 @@ var MISSING_TABLE = {
559
560
  * - PostgreSQL: `relation "sys_metadata_history" does not exist`
560
561
  * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
561
562
  */
562
- message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
563
+ message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
564
+ excludes: {
565
+ /**
566
+ * Exactly the three SQLSTATEs the docblock above already names as
567
+ * must-stay-loud neighbours of `does not exist`. They are listed here
568
+ * rather than merely trusted to miss the message test, because two of
569
+ * them (42703 columns, 42704 constraints/triggers) have a phrasing that
570
+ * *does* hit it, and because a code is a fact where prose is a guess.
571
+ *
572
+ * Postgres-shaped on purpose: measured, neither MySQL
573
+ * (`Unknown column 'label' in 'field list'`) nor SQLite
574
+ * (`no such column: bogus`, `table t has no column named label`)
575
+ * phrases a sub-object failure so that a missing-table phrase falls out
576
+ * of it, so there is nothing there to exclude. Adding their codes would
577
+ * be surface with no defect behind it.
578
+ */
579
+ codes: /* @__PURE__ */ new Set([
580
+ "42703",
581
+ // undefined_column
582
+ "42704",
583
+ // undefined_object — constraint, trigger, role, type, …
584
+ "3D000"
585
+ // invalid_catalog_name — `database "x" does not exist`
586
+ ]),
587
+ /**
588
+ * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
589
+ * failure about something *inside* a relation, which therefore says the
590
+ * relation itself is present. The two in-repo siblings that carry this
591
+ * phrase are `mapDataError` (`packages/rest`, #5352) and
592
+ * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
593
+ *
594
+ * [#6615] All three now read one home — `@objectstack/types` — instead
595
+ * of three hand-kept copies, so the phrase can no longer be taught to
596
+ * the repo a fourth time or drift in one package only. The **width**
597
+ * difference that used to justify the copy is preserved and is the
598
+ * reason the home exports two functions rather than one: those two
599
+ * *extract* the column name to phrase a better error, so a miss costs a
600
+ * vaguer message; this one *excludes*, so a miss restores the
601
+ * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
602
+ * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
603
+ * anchors: any sub-object, any quoted identifier, any verdict.
604
+ * Over-matching here only ever converts a benign verdict into a loud
605
+ * one, which is the direction this whole module already errs in.
606
+ */
607
+ matchesMessage: isRelationSubObjectPhrase
608
+ }
563
609
  };
564
610
  var MAX_CAUSE_DEPTH = 4;
565
611
  function matchesDriverError(error, signature, depth) {
566
612
  if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
567
- if (typeof error === "string") return signature.message.test(error);
613
+ if (typeof error === "string") {
614
+ if (signature.excludes?.matchesMessage(error)) return false;
615
+ return signature.message.test(error);
616
+ }
568
617
  if (typeof error !== "object") return false;
569
618
  const err = error;
619
+ const excludes = signature.excludes;
620
+ if (excludes) {
621
+ if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
622
+ if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
623
+ }
570
624
  if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
571
625
  if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
572
626
  if (typeof err.message === "string" && signature.message.test(err.message)) return true;
@@ -579,48 +633,6 @@ function isMissingTableError(error, depth = 0) {
579
633
  return matchesDriverError(error, MISSING_TABLE, depth);
580
634
  }
581
635
 
582
- // src/migrations/add-sys-metadata-overlay-index.ts
583
- var INDEX_NAME = "idx_sys_metadata_overlay_active";
584
- var TABLE = "sys_metadata";
585
- var COLUMNS = "(type, name, organization_id, environment_id, scope)";
586
- var WHERE = "state = 'active'";
587
- async function addSysMetadataOverlayIndex(driver) {
588
- const driverAny = driver;
589
- const exec = async (sql) => {
590
- if (typeof driverAny.raw === "function") {
591
- await driverAny.raw(sql);
592
- } else if (typeof driverAny.execute === "function") {
593
- await driverAny.execute(sql);
594
- } else {
595
- throw new Error("driver has neither raw nor execute");
596
- }
597
- };
598
- const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
599
- const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
600
- try {
601
- await exec(partialSql);
602
- return { index: INDEX_NAME, status: "created" };
603
- } catch (err) {
604
- const msg = err instanceof Error ? err.message : String(err);
605
- if (/partial|where clause|syntax/i.test(msg)) {
606
- try {
607
- await exec(fallbackSql);
608
- return { index: INDEX_NAME, status: "fallback_non_unique" };
609
- } catch (fallbackErr) {
610
- return {
611
- index: INDEX_NAME,
612
- status: "error",
613
- error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
614
- };
615
- }
616
- }
617
- if (/already exists/i.test(msg)) {
618
- return { index: INDEX_NAME, status: "already_exists" };
619
- }
620
- return { index: INDEX_NAME, status: "error", error: msg };
621
- }
622
- }
623
-
624
636
  // src/migrations/migrate-project-id-to-environment-id.ts
625
637
  var AFFECTED_TABLES = [
626
638
  "sys_metadata",
@@ -770,23 +782,40 @@ var DatabaseLoader = class {
770
782
  // ==========================================
771
783
  // Internal CRUD helpers (driver vs engine)
772
784
  // ==========================================
785
+ // NOTE (#6231, closed out by #7178): BOTH branches below now take `query`
786
+ // unchanged and uncast. `DriverQuery` is `Omit<QueryAST, 'object'>`, so the
787
+ // object name travels as argument one only — that was always enough for the
788
+ // driver branch. The ENGINE branch used to carry `as any`, for one reason:
789
+ // `EngineQueryOptionsSchema.search` admitted only the structured
790
+ // `FullTextSearchSchema`, while `QueryAST.search` (hence `DriverQuery`) also
791
+ // admits the bare query string that ADR-0061 D1 calls the canonical Tier-1
792
+ // spelling and that the engine actually serves, so `DriverQuery` was not
793
+ // assignable to `EngineQueryOptionsParsed`. #7178 aligned the two schemas;
794
+ // the casts are now genuinely vestigial and are gone, which restores real
795
+ // `where`/`orderBy`/`fields` checking on the metadata main read path — this
796
+ // schema is not `.strict()`, so an unknown key here is SILENTLY DROPPED
797
+ // (`check:query-options-erasure`'s own rationale) and the erased type was
798
+ // the only thing standing between a typo and that silence.
799
+ //
800
+ // If a future edit makes one of these stop compiling, the honest fix is to
801
+ // reconcile the two schemas again — not to reinstate the cast.
773
802
  async _find(table, query) {
774
803
  if (this.engine) {
775
804
  return this.engine.find(table, query);
776
805
  }
777
- return this.driver.find(table, { object: table, ...query });
806
+ return this.driver.find(table, query);
778
807
  }
779
808
  async _findOne(table, query) {
780
809
  if (this.engine) {
781
810
  return this.engine.findOne(table, query);
782
811
  }
783
- return this.driver.findOne(table, { object: table, ...query });
812
+ return this.driver.findOne(table, query);
784
813
  }
785
814
  async _count(table, query) {
786
815
  if (this.engine) {
787
816
  return this.engine.count(table, query);
788
817
  }
789
- return this.driver.count(table, { object: table, ...query });
818
+ return this.driver.count(table, query);
790
819
  }
791
820
  async _create(table, data) {
792
821
  if (this.engine) {
@@ -868,9 +897,12 @@ var DatabaseLoader = class {
868
897
  }
869
898
  if (driver) {
870
899
  await migrateProjectIdToEnvironmentId(driver).catch(() => void 0);
871
- await addSysMetadataOverlayIndex(driver);
872
900
  }
873
- } catch {
901
+ } catch (error) {
902
+ console.warn(
903
+ `[Metadata] Could not resolve a raw-SQL driver from the engine for \`${this.tableName}\` \u2014 the project_id\u2192environment_id forward migration was SKIPPED. Legacy rows (if any) keep the pre-v5.0 column and read back as unset. Metadata reads and writes are otherwise unaffected. Re-run it explicitly with \`migrateProjectIdToEnvironmentId(driver)\` from \`@objectstack/metadata/migrations\` once the datasource is reachable.`,
904
+ error
905
+ );
874
906
  }
875
907
  return;
876
908
  }
@@ -902,10 +934,6 @@ var DatabaseLoader = class {
902
934
  await migrateProjectIdToEnvironmentId(this.driver);
903
935
  } catch {
904
936
  }
905
- try {
906
- await addSysMetadataOverlayIndex(this.driver);
907
- } catch {
908
- }
909
937
  }
910
938
  /**
911
939
  * Ensure the history table exists.
@@ -1517,6 +1545,60 @@ import {
1517
1545
  identityFreeEndpointGateFailure,
1518
1546
  normalizeEndpointPath
1519
1547
  } from "@objectstack/spec/api";
1548
+
1549
+ // src/stored-envelope.ts
1550
+ var STORED_ENVELOPE_KEYS = Object.freeze([
1551
+ "package",
1552
+ "packageId",
1553
+ "publishedAt",
1554
+ "publishedBy",
1555
+ "publishedDefinition",
1556
+ "state",
1557
+ "version"
1558
+ ]);
1559
+ var STORED_BODY_KEY = "metadata";
1560
+ var ENVELOPE_KEYS = /* @__PURE__ */ new Set([...STORED_ENVELOPE_KEYS, STORED_BODY_KEY]);
1561
+ var EMPTY_ENVELOPE = Object.freeze({});
1562
+ function peelStoredEnvelope(item) {
1563
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
1564
+ return { envelope: EMPTY_ENVELOPE, body: item, wrapped: false };
1565
+ }
1566
+ const row = item;
1567
+ const wrappedBody = row[STORED_BODY_KEY];
1568
+ if (wrappedBody !== void 0 && wrappedBody !== null) {
1569
+ const envelope2 = {};
1570
+ for (const key of Object.keys(row)) {
1571
+ if (key === STORED_BODY_KEY) continue;
1572
+ envelope2[key] = row[key];
1573
+ }
1574
+ return { envelope: Object.freeze(envelope2), body: wrappedBody, wrapped: true };
1575
+ }
1576
+ let envelope;
1577
+ for (const key of Object.keys(row)) {
1578
+ if (!ENVELOPE_KEYS.has(key)) continue;
1579
+ envelope ?? (envelope = {});
1580
+ envelope[key] = row[key];
1581
+ }
1582
+ if (!envelope) return { envelope: EMPTY_ENVELOPE, body: row, wrapped: false };
1583
+ const body = {};
1584
+ for (const key of Object.keys(row)) {
1585
+ if (ENVELOPE_KEYS.has(key)) continue;
1586
+ body[key] = row[key];
1587
+ }
1588
+ return { envelope: Object.freeze(envelope), body, wrapped: false };
1589
+ }
1590
+ function storedItemName(peeled) {
1591
+ const fromEnvelope = peeled.envelope.name;
1592
+ if (typeof fromEnvelope === "string") return fromEnvelope;
1593
+ const body = peeled.body;
1594
+ if (body && typeof body === "object" && !Array.isArray(body)) {
1595
+ const fromBody = body.name;
1596
+ if (typeof fromBody === "string") return fromBody;
1597
+ }
1598
+ return void 0;
1599
+ }
1600
+
1601
+ // src/endpoint-matcher.ts
1520
1602
  function normalizeEndpointMethod(method) {
1521
1603
  return String(method ?? "").toUpperCase();
1522
1604
  }
@@ -1526,9 +1608,10 @@ function endpointIndexKey(method, path3) {
1526
1608
  function buildEndpointIndex(items, logger) {
1527
1609
  const index = /* @__PURE__ */ new Map();
1528
1610
  for (const item of items) {
1529
- const parsed = ApiEndpointSchema.safeParse(item);
1611
+ const peeled = peelStoredEnvelope(item);
1612
+ const parsed = ApiEndpointSchema.safeParse(peeled.body);
1530
1613
  if (!parsed.success) {
1531
- const declaredName = item && typeof item === "object" && typeof item.name === "string" ? item.name : "<unnamed>";
1614
+ const declaredName = storedItemName(peeled) ?? "<unnamed>";
1532
1615
  logger.error(
1533
1616
  `[EndpointMatcher] stored api item '${declaredName}' does not satisfy ApiEndpointSchema \u2014 it is EXCLUDED from endpoint matching and its declared route will answer 404. Fix the declaration (or remove it); the endpoint index never serves a half-valid shape.`,
1534
1617
  void 0,
@@ -2027,16 +2110,30 @@ var _MetadataManager = class _MetadataManager {
2027
2110
  * has with {@link loadDiagnosed}, so every existing caller keeps its exact
2028
2111
  * behaviour and only callers that ASK for the verdict pay for it.
2029
2112
  *
2030
- * [#5840] Deliberately NOT expressed as `(await getDiagnosed(…)).data`,
2031
- * although that is what it computes. The obvious delegation adds one
2032
- * `await` hop, and a registry hit here is observed one microtask sooner than
2033
- * it would be through a second async frame — which `register()`'s watchers
2034
- * depend on, because `notifyWatchers` does not await its handlers and
2035
- * ObjectQL's bridge re-reads through `get()` on the event rather than
2036
- * trusting the payload (`register-notifies-watchers.test.ts` pins it, and
2037
- * went red on the delegating version). The duplication is three lines and is
2038
- * pinned from the other side: `get()` and `getDiagnosed().data` are asserted
2039
- * to agree on every case in `metadata-manager-get-diagnosed.test.ts`.
2113
+ * Not expressed as `(await getDiagnosed(…)).data`, although that is what it
2114
+ * computes and the reason has CHANGED, so do not read the duplication as a
2115
+ * standing constraint.
2116
+ *
2117
+ * [#5840] recorded the delegation as unsafe: it adds one `await` hop, and
2118
+ * `register-notifies-watchers.test.ts` went red on the delegating version, so
2119
+ * three lines were duplicated to hold the frame count fixed. [#6043] measured
2120
+ * that test and found it was pinning this method's microtask depth rather than
2121
+ * the ordering guarantee it named `notifyWatchers` never awaits its handlers,
2122
+ * so a subscriber's `await get(…)` had simply been settling inside the
2123
+ * microtasks `await register(…)` yields. That case now asserts the ordering
2124
+ * synchronously against the registry and does not observe this method's frame
2125
+ * count at all; the whole `@objectstack/metadata` suite was re-measured on the
2126
+ * delegating version and stayed green.
2127
+ *
2128
+ * What survives is a plain, local reason: the registry hit is the hot path and
2129
+ * answering it without a second async frame is worth three lines. Nothing
2130
+ * external depends on the hop count any more. Consolidating the two into one
2131
+ * delegation is therefore a viable, deliberately un-taken change (#6043 was
2132
+ * test-scoped) — if you take it, note that `get()`'s callers outside this
2133
+ * package were never surveyed for timing sensitivity, only this package's
2134
+ * tests. Either way the two stay pinned to each other from the other side:
2135
+ * `get()` and `getDiagnosed().data` are asserted to agree on every case in
2136
+ * `metadata-manager-get-diagnosed.test.ts`.
2040
2137
  */
2041
2138
  async get(type, name) {
2042
2139
  const typeStore = this.registry.get(type);
@@ -2712,10 +2809,16 @@ var _MetadataManager = class _MetadataManager {
2712
2809
  * ## What it judges, and on what
2713
2810
  *
2714
2811
  * The registry stores either a raw spec document or a publish envelope
2715
- * (`{ name, packageId, state, metadata: {…spec} }`); the endpoint is read
2716
- * out with the SAME rule this method's caller uses for
2717
- * `publishedDefinition` (`data.metadata ?? data`), so publish gates exactly
2718
- * the document publish is about to snapshot. An item that does not satisfy
2812
+ * (`{ name, packageId, state, metadata: {…spec} }`), and in BOTH shapes the
2813
+ * row carries the metadata layer's bookkeeping. [#5309] The envelope is
2814
+ * peeled off first (`peelStoredEnvelope`) and the gate judges the authored
2815
+ * BODY: the wrapped half of that peel is the `data.metadata ?? data` rule
2816
+ * this method used to spell inline — the same document `publishedDefinition`
2817
+ * snapshots — and the flat half additionally removes `packageId` / `state` /
2818
+ * `version` / `published*`, which are storage identity, never endpoint
2819
+ * vocabulary. (What publish SNAPSHOTS is unchanged: `publishedDefinition`
2820
+ * still stores `data.metadata ?? data` verbatim, envelope included, because
2821
+ * `revertPackage` restores from it.) An item whose body does not satisfy
2719
2822
  * `ApiEndpointSchema` fails here too — not extra strictness but a
2720
2823
  * precondition: an unparsed shape cannot be gated, and it could never be
2721
2824
  * served either (the matcher's own loud skip refuses it at load).
@@ -2733,8 +2836,8 @@ var _MetadataManager = class _MetadataManager {
2733
2836
  const endpoints = [];
2734
2837
  const gatedItems = [];
2735
2838
  for (const item of apiItems) {
2736
- const document = item.data?.metadata ?? item.data;
2737
- const parsed = ApiEndpointSchema2.safeParse(document);
2839
+ const { body } = peelStoredEnvelope(item.data);
2840
+ const parsed = ApiEndpointSchema2.safeParse(body);
2738
2841
  if (!parsed.success) {
2739
2842
  for (const issue of parsed.error.issues) {
2740
2843
  errors.push({
@@ -4324,8 +4427,16 @@ var ARTIFACT_FIELD_TO_TYPE = {
4324
4427
  connectors: "connector",
4325
4428
  emailTemplates: "email_template",
4326
4429
  docs: "doc",
4327
- books: "book",
4328
- data: "dataset"
4430
+ books: "book"
4431
+ // `data:` (the SEED collection) is deliberately absent — #6242 row 4(a).
4432
+ // It used to map to `'dataset'`, the ADR-0021 analytics kind: the exact name
4433
+ // collision `metadata-plugin.zod.ts` warns about in prose. The entry never
4434
+ // registered anything (SeedSchema declares no `name`, and the loop below
4435
+ // skips nameless items) — a dead pointer aimed at the wrong kind, which
4436
+ // would have begun mis-registering the day either side moved. Removed rather
4437
+ // than repointed at `'seed'`: seeds are APPLIED by SeedLoaderService off the
4438
+ // bundle, never registered as metadata items, so a `seed` mapping would be
4439
+ // new behaviour rather than a corrected name.
4329
4440
  };
4330
4441
  var MetadataPlugin = class {
4331
4442
  constructor(options = {}) {