@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.cjs CHANGED
@@ -555,6 +555,7 @@ var LRUCache = class {
555
555
  };
556
556
 
557
557
  // src/utils/schema-sync-errors.ts
558
+ var import_types = require("@objectstack/types");
558
559
  var ALREADY_EXISTS = {
559
560
  codes: /* @__PURE__ */ new Set([
560
561
  // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
@@ -598,14 +599,67 @@ var MISSING_TABLE = {
598
599
  * - PostgreSQL: `relation "sys_metadata_history" does not exist`
599
600
  * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
600
601
  */
601
- message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
602
+ message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
603
+ excludes: {
604
+ /**
605
+ * Exactly the three SQLSTATEs the docblock above already names as
606
+ * must-stay-loud neighbours of `does not exist`. They are listed here
607
+ * rather than merely trusted to miss the message test, because two of
608
+ * them (42703 columns, 42704 constraints/triggers) have a phrasing that
609
+ * *does* hit it, and because a code is a fact where prose is a guess.
610
+ *
611
+ * Postgres-shaped on purpose: measured, neither MySQL
612
+ * (`Unknown column 'label' in 'field list'`) nor SQLite
613
+ * (`no such column: bogus`, `table t has no column named label`)
614
+ * phrases a sub-object failure so that a missing-table phrase falls out
615
+ * of it, so there is nothing there to exclude. Adding their codes would
616
+ * be surface with no defect behind it.
617
+ */
618
+ codes: /* @__PURE__ */ new Set([
619
+ "42703",
620
+ // undefined_column
621
+ "42704",
622
+ // undefined_object — constraint, trigger, role, type, …
623
+ "3D000"
624
+ // invalid_catalog_name — `database "x" does not exist`
625
+ ]),
626
+ /**
627
+ * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
628
+ * failure about something *inside* a relation, which therefore says the
629
+ * relation itself is present. The two in-repo siblings that carry this
630
+ * phrase are `mapDataError` (`packages/rest`, #5352) and
631
+ * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
632
+ *
633
+ * [#6615] All three now read one home — `@objectstack/types` — instead
634
+ * of three hand-kept copies, so the phrase can no longer be taught to
635
+ * the repo a fourth time or drift in one package only. The **width**
636
+ * difference that used to justify the copy is preserved and is the
637
+ * reason the home exports two functions rather than one: those two
638
+ * *extract* the column name to phrase a better error, so a miss costs a
639
+ * vaguer message; this one *excludes*, so a miss restores the
640
+ * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
641
+ * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
642
+ * anchors: any sub-object, any quoted identifier, any verdict.
643
+ * Over-matching here only ever converts a benign verdict into a loud
644
+ * one, which is the direction this whole module already errs in.
645
+ */
646
+ matchesMessage: import_types.isRelationSubObjectPhrase
647
+ }
602
648
  };
603
649
  var MAX_CAUSE_DEPTH = 4;
604
650
  function matchesDriverError(error, signature, depth) {
605
651
  if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
606
- if (typeof error === "string") return signature.message.test(error);
652
+ if (typeof error === "string") {
653
+ if (signature.excludes?.matchesMessage(error)) return false;
654
+ return signature.message.test(error);
655
+ }
607
656
  if (typeof error !== "object") return false;
608
657
  const err = error;
658
+ const excludes = signature.excludes;
659
+ if (excludes) {
660
+ if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
661
+ if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
662
+ }
609
663
  if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
610
664
  if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
611
665
  if (typeof err.message === "string" && signature.message.test(err.message)) return true;
@@ -618,48 +672,6 @@ function isMissingTableError(error, depth = 0) {
618
672
  return matchesDriverError(error, MISSING_TABLE, depth);
619
673
  }
620
674
 
621
- // src/migrations/add-sys-metadata-overlay-index.ts
622
- var INDEX_NAME = "idx_sys_metadata_overlay_active";
623
- var TABLE = "sys_metadata";
624
- var COLUMNS = "(type, name, organization_id, environment_id, scope)";
625
- var WHERE = "state = 'active'";
626
- async function addSysMetadataOverlayIndex(driver) {
627
- const driverAny = driver;
628
- const exec = async (sql) => {
629
- if (typeof driverAny.raw === "function") {
630
- await driverAny.raw(sql);
631
- } else if (typeof driverAny.execute === "function") {
632
- await driverAny.execute(sql);
633
- } else {
634
- throw new Error("driver has neither raw nor execute");
635
- }
636
- };
637
- const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
638
- const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
639
- try {
640
- await exec(partialSql);
641
- return { index: INDEX_NAME, status: "created" };
642
- } catch (err) {
643
- const msg = err instanceof Error ? err.message : String(err);
644
- if (/partial|where clause|syntax/i.test(msg)) {
645
- try {
646
- await exec(fallbackSql);
647
- return { index: INDEX_NAME, status: "fallback_non_unique" };
648
- } catch (fallbackErr) {
649
- return {
650
- index: INDEX_NAME,
651
- status: "error",
652
- error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
653
- };
654
- }
655
- }
656
- if (/already exists/i.test(msg)) {
657
- return { index: INDEX_NAME, status: "already_exists" };
658
- }
659
- return { index: INDEX_NAME, status: "error", error: msg };
660
- }
661
- }
662
-
663
675
  // src/migrations/migrate-project-id-to-environment-id.ts
664
676
  var AFFECTED_TABLES = [
665
677
  "sys_metadata",
@@ -809,23 +821,40 @@ var DatabaseLoader = class {
809
821
  // ==========================================
810
822
  // Internal CRUD helpers (driver vs engine)
811
823
  // ==========================================
824
+ // NOTE (#6231, closed out by #7178): BOTH branches below now take `query`
825
+ // unchanged and uncast. `DriverQuery` is `Omit<QueryAST, 'object'>`, so the
826
+ // object name travels as argument one only — that was always enough for the
827
+ // driver branch. The ENGINE branch used to carry `as any`, for one reason:
828
+ // `EngineQueryOptionsSchema.search` admitted only the structured
829
+ // `FullTextSearchSchema`, while `QueryAST.search` (hence `DriverQuery`) also
830
+ // admits the bare query string that ADR-0061 D1 calls the canonical Tier-1
831
+ // spelling and that the engine actually serves, so `DriverQuery` was not
832
+ // assignable to `EngineQueryOptionsParsed`. #7178 aligned the two schemas;
833
+ // the casts are now genuinely vestigial and are gone, which restores real
834
+ // `where`/`orderBy`/`fields` checking on the metadata main read path — this
835
+ // schema is not `.strict()`, so an unknown key here is SILENTLY DROPPED
836
+ // (`check:query-options-erasure`'s own rationale) and the erased type was
837
+ // the only thing standing between a typo and that silence.
838
+ //
839
+ // If a future edit makes one of these stop compiling, the honest fix is to
840
+ // reconcile the two schemas again — not to reinstate the cast.
812
841
  async _find(table, query) {
813
842
  if (this.engine) {
814
843
  return this.engine.find(table, query);
815
844
  }
816
- return this.driver.find(table, { object: table, ...query });
845
+ return this.driver.find(table, query);
817
846
  }
818
847
  async _findOne(table, query) {
819
848
  if (this.engine) {
820
849
  return this.engine.findOne(table, query);
821
850
  }
822
- return this.driver.findOne(table, { object: table, ...query });
851
+ return this.driver.findOne(table, query);
823
852
  }
824
853
  async _count(table, query) {
825
854
  if (this.engine) {
826
855
  return this.engine.count(table, query);
827
856
  }
828
- return this.driver.count(table, { object: table, ...query });
857
+ return this.driver.count(table, query);
829
858
  }
830
859
  async _create(table, data) {
831
860
  if (this.engine) {
@@ -907,9 +936,12 @@ var DatabaseLoader = class {
907
936
  }
908
937
  if (driver) {
909
938
  await migrateProjectIdToEnvironmentId(driver).catch(() => void 0);
910
- await addSysMetadataOverlayIndex(driver);
911
939
  }
912
- } catch {
940
+ } catch (error) {
941
+ console.warn(
942
+ `[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.`,
943
+ error
944
+ );
913
945
  }
914
946
  return;
915
947
  }
@@ -941,10 +973,6 @@ var DatabaseLoader = class {
941
973
  await migrateProjectIdToEnvironmentId(this.driver);
942
974
  } catch {
943
975
  }
944
- try {
945
- await addSysMetadataOverlayIndex(this.driver);
946
- } catch {
947
- }
948
976
  }
949
977
  /**
950
978
  * Ensure the history table exists.
@@ -1552,6 +1580,60 @@ function generateId() {
1552
1580
 
1553
1581
  // src/endpoint-matcher.ts
1554
1582
  var import_api = require("@objectstack/spec/api");
1583
+
1584
+ // src/stored-envelope.ts
1585
+ var STORED_ENVELOPE_KEYS = Object.freeze([
1586
+ "package",
1587
+ "packageId",
1588
+ "publishedAt",
1589
+ "publishedBy",
1590
+ "publishedDefinition",
1591
+ "state",
1592
+ "version"
1593
+ ]);
1594
+ var STORED_BODY_KEY = "metadata";
1595
+ var ENVELOPE_KEYS = /* @__PURE__ */ new Set([...STORED_ENVELOPE_KEYS, STORED_BODY_KEY]);
1596
+ var EMPTY_ENVELOPE = Object.freeze({});
1597
+ function peelStoredEnvelope(item) {
1598
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
1599
+ return { envelope: EMPTY_ENVELOPE, body: item, wrapped: false };
1600
+ }
1601
+ const row = item;
1602
+ const wrappedBody = row[STORED_BODY_KEY];
1603
+ if (wrappedBody !== void 0 && wrappedBody !== null) {
1604
+ const envelope2 = {};
1605
+ for (const key of Object.keys(row)) {
1606
+ if (key === STORED_BODY_KEY) continue;
1607
+ envelope2[key] = row[key];
1608
+ }
1609
+ return { envelope: Object.freeze(envelope2), body: wrappedBody, wrapped: true };
1610
+ }
1611
+ let envelope;
1612
+ for (const key of Object.keys(row)) {
1613
+ if (!ENVELOPE_KEYS.has(key)) continue;
1614
+ envelope ?? (envelope = {});
1615
+ envelope[key] = row[key];
1616
+ }
1617
+ if (!envelope) return { envelope: EMPTY_ENVELOPE, body: row, wrapped: false };
1618
+ const body = {};
1619
+ for (const key of Object.keys(row)) {
1620
+ if (ENVELOPE_KEYS.has(key)) continue;
1621
+ body[key] = row[key];
1622
+ }
1623
+ return { envelope: Object.freeze(envelope), body, wrapped: false };
1624
+ }
1625
+ function storedItemName(peeled) {
1626
+ const fromEnvelope = peeled.envelope.name;
1627
+ if (typeof fromEnvelope === "string") return fromEnvelope;
1628
+ const body = peeled.body;
1629
+ if (body && typeof body === "object" && !Array.isArray(body)) {
1630
+ const fromBody = body.name;
1631
+ if (typeof fromBody === "string") return fromBody;
1632
+ }
1633
+ return void 0;
1634
+ }
1635
+
1636
+ // src/endpoint-matcher.ts
1555
1637
  function normalizeEndpointMethod(method) {
1556
1638
  return String(method ?? "").toUpperCase();
1557
1639
  }
@@ -1561,9 +1643,10 @@ function endpointIndexKey(method, path3) {
1561
1643
  function buildEndpointIndex(items, logger) {
1562
1644
  const index = /* @__PURE__ */ new Map();
1563
1645
  for (const item of items) {
1564
- const parsed = import_api.ApiEndpointSchema.safeParse(item);
1646
+ const peeled = peelStoredEnvelope(item);
1647
+ const parsed = import_api.ApiEndpointSchema.safeParse(peeled.body);
1565
1648
  if (!parsed.success) {
1566
- const declaredName = item && typeof item === "object" && typeof item.name === "string" ? item.name : "<unnamed>";
1649
+ const declaredName = storedItemName(peeled) ?? "<unnamed>";
1567
1650
  logger.error(
1568
1651
  `[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.`,
1569
1652
  void 0,
@@ -2062,16 +2145,30 @@ var _MetadataManager = class _MetadataManager {
2062
2145
  * has with {@link loadDiagnosed}, so every existing caller keeps its exact
2063
2146
  * behaviour and only callers that ASK for the verdict pay for it.
2064
2147
  *
2065
- * [#5840] Deliberately NOT expressed as `(await getDiagnosed(…)).data`,
2066
- * although that is what it computes. The obvious delegation adds one
2067
- * `await` hop, and a registry hit here is observed one microtask sooner than
2068
- * it would be through a second async frame — which `register()`'s watchers
2069
- * depend on, because `notifyWatchers` does not await its handlers and
2070
- * ObjectQL's bridge re-reads through `get()` on the event rather than
2071
- * trusting the payload (`register-notifies-watchers.test.ts` pins it, and
2072
- * went red on the delegating version). The duplication is three lines and is
2073
- * pinned from the other side: `get()` and `getDiagnosed().data` are asserted
2074
- * to agree on every case in `metadata-manager-get-diagnosed.test.ts`.
2148
+ * Not expressed as `(await getDiagnosed(…)).data`, although that is what it
2149
+ * computes and the reason has CHANGED, so do not read the duplication as a
2150
+ * standing constraint.
2151
+ *
2152
+ * [#5840] recorded the delegation as unsafe: it adds one `await` hop, and
2153
+ * `register-notifies-watchers.test.ts` went red on the delegating version, so
2154
+ * three lines were duplicated to hold the frame count fixed. [#6043] measured
2155
+ * that test and found it was pinning this method's microtask depth rather than
2156
+ * the ordering guarantee it named `notifyWatchers` never awaits its handlers,
2157
+ * so a subscriber's `await get(…)` had simply been settling inside the
2158
+ * microtasks `await register(…)` yields. That case now asserts the ordering
2159
+ * synchronously against the registry and does not observe this method's frame
2160
+ * count at all; the whole `@objectstack/metadata` suite was re-measured on the
2161
+ * delegating version and stayed green.
2162
+ *
2163
+ * What survives is a plain, local reason: the registry hit is the hot path and
2164
+ * answering it without a second async frame is worth three lines. Nothing
2165
+ * external depends on the hop count any more. Consolidating the two into one
2166
+ * delegation is therefore a viable, deliberately un-taken change (#6043 was
2167
+ * test-scoped) — if you take it, note that `get()`'s callers outside this
2168
+ * package were never surveyed for timing sensitivity, only this package's
2169
+ * tests. Either way the two stay pinned to each other from the other side:
2170
+ * `get()` and `getDiagnosed().data` are asserted to agree on every case in
2171
+ * `metadata-manager-get-diagnosed.test.ts`.
2075
2172
  */
2076
2173
  async get(type, name) {
2077
2174
  const typeStore = this.registry.get(type);
@@ -2747,10 +2844,16 @@ var _MetadataManager = class _MetadataManager {
2747
2844
  * ## What it judges, and on what
2748
2845
  *
2749
2846
  * The registry stores either a raw spec document or a publish envelope
2750
- * (`{ name, packageId, state, metadata: {…spec} }`); the endpoint is read
2751
- * out with the SAME rule this method's caller uses for
2752
- * `publishedDefinition` (`data.metadata ?? data`), so publish gates exactly
2753
- * the document publish is about to snapshot. An item that does not satisfy
2847
+ * (`{ name, packageId, state, metadata: {…spec} }`), and in BOTH shapes the
2848
+ * row carries the metadata layer's bookkeeping. [#5309] The envelope is
2849
+ * peeled off first (`peelStoredEnvelope`) and the gate judges the authored
2850
+ * BODY: the wrapped half of that peel is the `data.metadata ?? data` rule
2851
+ * this method used to spell inline — the same document `publishedDefinition`
2852
+ * snapshots — and the flat half additionally removes `packageId` / `state` /
2853
+ * `version` / `published*`, which are storage identity, never endpoint
2854
+ * vocabulary. (What publish SNAPSHOTS is unchanged: `publishedDefinition`
2855
+ * still stores `data.metadata ?? data` verbatim, envelope included, because
2856
+ * `revertPackage` restores from it.) An item whose body does not satisfy
2754
2857
  * `ApiEndpointSchema` fails here too — not extra strictness but a
2755
2858
  * precondition: an unparsed shape cannot be gated, and it could never be
2756
2859
  * served either (the matcher's own loud skip refuses it at load).
@@ -2768,8 +2871,8 @@ var _MetadataManager = class _MetadataManager {
2768
2871
  const endpoints = [];
2769
2872
  const gatedItems = [];
2770
2873
  for (const item of apiItems) {
2771
- const document = item.data?.metadata ?? item.data;
2772
- const parsed = import_api3.ApiEndpointSchema.safeParse(document);
2874
+ const { body } = peelStoredEnvelope(item.data);
2875
+ const parsed = import_api3.ApiEndpointSchema.safeParse(body);
2773
2876
  if (!parsed.success) {
2774
2877
  for (const issue of parsed.error.issues) {
2775
2878
  errors.push({
@@ -4353,8 +4456,16 @@ var ARTIFACT_FIELD_TO_TYPE = {
4353
4456
  connectors: "connector",
4354
4457
  emailTemplates: "email_template",
4355
4458
  docs: "doc",
4356
- books: "book",
4357
- data: "dataset"
4459
+ books: "book"
4460
+ // `data:` (the SEED collection) is deliberately absent — #6242 row 4(a).
4461
+ // It used to map to `'dataset'`, the ADR-0021 analytics kind: the exact name
4462
+ // collision `metadata-plugin.zod.ts` warns about in prose. The entry never
4463
+ // registered anything (SeedSchema declares no `name`, and the loop below
4464
+ // skips nameless items) — a dead pointer aimed at the wrong kind, which
4465
+ // would have begun mis-registering the day either side moved. Removed rather
4466
+ // than repointed at `'seed'`: seeds are APPLIED by SeedLoaderService off the
4467
+ // bundle, never registered as metadata items, so a `seed` mapping would be
4468
+ // new behaviour rather than a corrected name.
4358
4469
  };
4359
4470
  var MetadataPlugin = class {
4360
4471
  constructor(options = {}) {