@objectstack/types 17.2.0 → 17.4.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/dist/index.mjs CHANGED
@@ -15,6 +15,12 @@ function emitDegradedBootBanner(message) {
15
15
  }
16
16
  }
17
17
 
18
+ // src/email-verified.ts
19
+ function isEmailVerifiedUserRow(row) {
20
+ const v = row?.email_verified;
21
+ return v === true || v === 1 || v === "1" || v === "true";
22
+ }
23
+
18
24
  // src/env.ts
19
25
  import {
20
26
  normalizeTenancyPosture,
@@ -63,6 +69,13 @@ function resolveTenancyPosture() {
63
69
  }
64
70
  return resolveMultiOrgEnabled() ? "isolated" : "single";
65
71
  }
72
+ var PLATFORM_OWNER_EMAIL_ENV = "OS_PLATFORM_OWNER_EMAIL";
73
+ function resolvePlatformOwnerEmail() {
74
+ const raw = globalThis.process?.env?.[PLATFORM_OWNER_EMAIL_ENV];
75
+ if (raw == null) return void 0;
76
+ const trimmed = String(raw).trim();
77
+ return trimmed === "" ? void 0 : trimmed;
78
+ }
66
79
  function resolveAllowDegradedTenancy() {
67
80
  const raw = readEnvWithDeprecation("OS_ALLOW_DEGRADED_TENANCY", [], { silent: true });
68
81
  if (raw == null) return false;
@@ -102,6 +115,12 @@ function resolveOrgLimit() {
102
115
  const n = Number.parseInt(String(raw), 10);
103
116
  return Number.isFinite(n) && n > 0 ? n : void 0;
104
117
  }
118
+ function resolveOrgMembershipLimit() {
119
+ const raw = readEnvWithDeprecation("OS_ORG_MEMBERSHIP_LIMIT", [], { silent: true });
120
+ if (raw == null || String(raw).trim() === "") return void 0;
121
+ const n = Number.parseInt(String(raw), 10);
122
+ return Number.isFinite(n) && n > 0 ? n : void 0;
123
+ }
105
124
  function resolveSearchPinyinEnabled(opts) {
106
125
  const raw = readEnvWithDeprecation("OS_SEARCH_PINYIN_ENABLED", [], { silent: true });
107
126
  if (raw != null && String(raw).trim() !== "") {
@@ -253,11 +272,78 @@ function isModuleNotFoundError(err) {
253
272
  return msg.includes("Cannot find module") || msg.includes("Cannot find package");
254
273
  }
255
274
 
275
+ // src/server-fault-log.ts
276
+ var SERVER_FAULT_LOG_PREFIX = "[5xx]";
277
+ function isServerFault(status) {
278
+ return typeof status === "number" && status >= 500;
279
+ }
280
+ function toError(thrown) {
281
+ if (thrown === void 0 || thrown === null) return void 0;
282
+ if (thrown instanceof Error) return thrown;
283
+ const wrapped = new Error(typeof thrown === "string" ? thrown : safeStringify(thrown));
284
+ wrapped.stack = void 0;
285
+ return wrapped;
286
+ }
287
+ function safeStringify(value) {
288
+ try {
289
+ return JSON.stringify(value) ?? String(value);
290
+ } catch {
291
+ return String(value);
292
+ }
293
+ }
294
+ function serverFaultLogMessage(input) {
295
+ const err = toError(input.error);
296
+ const text = err?.message || input.message || "Unhandled server fault";
297
+ const where = [input.request?.method, input.request?.path].filter(Boolean).join(" ");
298
+ return `${SERVER_FAULT_LOG_PREFIX} ${input.status}${where ? ` ${where}` : ""} \u2014 ${text}`;
299
+ }
300
+ function serverFaultLogMeta(input) {
301
+ return {
302
+ status: input.status,
303
+ ...input.code !== void 0 ? { code: input.code } : {},
304
+ ...input.request?.method !== void 0 ? { method: input.request.method } : {},
305
+ ...input.request?.path !== void 0 ? { path: input.request.path } : {},
306
+ ...input.request?.requestId !== void 0 ? { requestId: input.request.requestId } : {}
307
+ };
308
+ }
309
+ function logServerFault(input, logger) {
310
+ if (!isServerFault(input.status)) return false;
311
+ const message = serverFaultLogMessage(input);
312
+ const meta = serverFaultLogMeta(input);
313
+ const err = toError(input.error);
314
+ try {
315
+ if (logger) {
316
+ logger.error(message, err, meta);
317
+ return true;
318
+ }
319
+ const sink = globalThis.console;
320
+ sink?.error?.(message, { ...meta, ...err?.stack ? { stack: err.stack } : {} });
321
+ return true;
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+ function describeFaultRequest(req) {
327
+ const r = req;
328
+ if (!r || typeof r !== "object") return {};
329
+ const str = (v) => typeof v === "string" && v ? v : void 0;
330
+ const headerId = r.headers ? str(r.headers["x-request-id"]) ?? str(r.headers["X-Request-Id"]) : void 0;
331
+ const method = str(r.method);
332
+ const path = str(r.path) ?? str(r.url) ?? str(r.originalUrl);
333
+ const requestId = str(r.requestId) ?? headerId;
334
+ return {
335
+ ...method !== void 0 ? { method } : {},
336
+ ...path !== void 0 ? { path } : {},
337
+ ...requestId !== void 0 ? { requestId } : {}
338
+ };
339
+ }
340
+
256
341
  // src/response-envelope.ts
257
342
  function sendOk(res, data, status = 200) {
258
343
  res.status(status).json({ success: true, data });
259
344
  }
260
345
  function sendError(res, status, code, message, extra) {
346
+ logServerFault({ status, code, message, ...extra?.requestId ? { request: { requestId: extra.requestId } } : {} });
261
347
  res.status(status).json({ success: false, error: { code, message, ...extra } });
262
348
  }
263
349
 
@@ -322,10 +408,33 @@ function declaredUserMessage(error) {
322
408
  const declared = error?.userMessage;
323
409
  return typeof declared === "string" && declared.trim().length > 0 ? declared : void 0;
324
410
  }
411
+ function serverFaultProvenance(thrown) {
412
+ if (thrown.status < 500) return void 0;
413
+ return thrown.declaredStatus === void 0 ? "undeclared" : "declared";
414
+ }
325
415
  function demotedDeclaredCode(thrown) {
416
+ if (serverFaultProvenance(thrown) === "undeclared") return void 0;
326
417
  return thrown.declaredCode !== void 0 && thrown.declaredCode !== thrown.code ? thrown.declaredCode : void 0;
327
418
  }
328
419
 
420
+ // src/stranded-decision.ts
421
+ var CARRIER = "strandedDecision";
422
+ function strandedDecisionDetails(err) {
423
+ const carried = err?.[CARRIER];
424
+ if (!carried || typeof carried !== "object") return void 0;
425
+ const d = carried;
426
+ if (d.finalized !== true) return void 0;
427
+ if (typeof d.decision !== "string" || d.decision === "") return void 0;
428
+ if (typeof d.runId !== "string" || d.runId === "") return void 0;
429
+ if (typeof d.repairable !== "boolean") return void 0;
430
+ return { finalized: true, decision: d.decision, runId: d.runId, repairable: d.repairable };
431
+ }
432
+ function strandedDecisionFailure(message, details) {
433
+ const err = new Error(message);
434
+ err[CARRIER] = details;
435
+ return err;
436
+ }
437
+
329
438
  // src/relation-sub-object.ts
330
439
  function matchMissingColumnOfRelation(message) {
331
440
  return MISSING_COLUMN_OF_RELATION.exec(message)?.[1];
@@ -338,7 +447,7 @@ var RELATION_SUB_OBJECT = /["'`][^"'`]+["'`]\s+of relation\s/i;
338
447
 
339
448
  // src/unique-violation.ts
340
449
  var UNIQUE_VIOLATION = {
341
- codes: /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT_UNIQUE"]),
450
+ codes: /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT_UNIQUE", "UNIQUE_VIOLATION"]),
342
451
  errnos: /* @__PURE__ */ new Set([1062]),
343
452
  message: /unique constraint failed|violates unique constraint|unique violation|duplicate key|duplicate entry/i
344
453
  };
@@ -402,16 +511,199 @@ function uniqueViolationColumn(error) {
402
511
  return findUniqueViolationColumn(error, 0);
403
512
  }
404
513
 
514
+ // src/driver-error-classification.ts
515
+ var RELATION_IN_PHRASE = [
516
+ // SQLite / libsql: `no such table: sys_metadata_history`, and the
517
+ // schema-qualified `no such table: main.orders` it uses when it resolved
518
+ // the name itself (views, triggers) or the caller qualified it.
519
+ /no such table:\s*([^\s'"`;,()]+)/i,
520
+ // PostgreSQL: `relation "sys_metadata_history" does not exist`
521
+ /relation\s+["'`]([^"'`]+)["'`]\s+does not exist/i,
522
+ // MySQL / MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
523
+ /table\s+["'`]([^"'`]+)["'`]\s+doesn'?t exist/i,
524
+ // MySQL / MariaDB: `Unknown table 'app.t'`
525
+ /unknown table\s+["'`]([^"'`]+)["'`]/i
526
+ ];
527
+ function normaliseRelationName(name) {
528
+ const afterQualifier = name.slice(name.lastIndexOf(".") + 1);
529
+ const namespaceEnd = afterQualifier.lastIndexOf("__");
530
+ const bare = namespaceEnd === -1 ? afterQualifier : afterQualifier.slice(namespaceEnd + 2);
531
+ return bare.toLowerCase();
532
+ }
533
+ function phraseNamesAnotherRelation(message, readObject) {
534
+ const expected = normaliseRelationName(readObject);
535
+ if (expected === "") return false;
536
+ let named = false;
537
+ for (const pattern of RELATION_IN_PHRASE) {
538
+ const captured = pattern.exec(message)?.[1];
539
+ if (captured === void 0) continue;
540
+ const candidate = normaliseRelationName(captured);
541
+ if (candidate === "") continue;
542
+ if (candidate === expected) return false;
543
+ named = true;
544
+ }
545
+ return named;
546
+ }
547
+ var ALREADY_EXISTS = {
548
+ codes: /* @__PURE__ */ new Set([
549
+ // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
550
+ "42P07",
551
+ // duplicate_table
552
+ "42701",
553
+ // duplicate_column
554
+ "42710",
555
+ // duplicate_object — index / constraint already exists
556
+ // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
557
+ "ER_TABLE_EXISTS_ERROR",
558
+ // 1050
559
+ "ER_DUP_FIELDNAME",
560
+ // 1060
561
+ "ER_DUP_KEYNAME"
562
+ // 1061
563
+ ]),
564
+ errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
565
+ /**
566
+ * Message fallback for drivers that carry no machine-readable code —
567
+ * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
568
+ * every DDL failure, so the message is the only signal available:
569
+ * - `table sys_metadata already exists`
570
+ * - `duplicate column name: environment_id`
571
+ * - `index idx_x already exists`
572
+ * Postgres phrases its own as `relation "x" already exists` /
573
+ * `column "x" of relation "y" already exists`, which matches the same test.
574
+ */
575
+ message: /already exists|duplicate column name|duplicate key name/i
576
+ };
577
+ var MISSING_TABLE = {
578
+ codes: /* @__PURE__ */ new Set([
579
+ "42P01",
580
+ // PostgreSQL undefined_table
581
+ "ER_NO_SUCH_TABLE"
582
+ // MySQL / MariaDB 1146
583
+ ]),
584
+ errnos: /* @__PURE__ */ new Set([1146]),
585
+ /**
586
+ * - SQLite / libsql: `no such table: sys_metadata_history`
587
+ * - PostgreSQL: `relation "sys_metadata_history" does not exist`
588
+ * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
589
+ */
590
+ message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
591
+ excludes: {
592
+ /**
593
+ * Exactly the three SQLSTATEs the docblock above already names as
594
+ * must-stay-loud neighbours of `does not exist`. They are listed here
595
+ * rather than merely trusted to miss the message test, because two of
596
+ * them (42703 columns, 42704 constraints/triggers) have a phrasing that
597
+ * *does* hit it, and because a code is a fact where prose is a guess.
598
+ *
599
+ * Postgres-shaped on purpose: measured, neither MySQL
600
+ * (`Unknown column 'label' in 'field list'`) nor SQLite
601
+ * (`no such column: bogus`, `table t has no column named label`)
602
+ * phrases a sub-object failure so that a missing-table phrase falls out
603
+ * of it, so there is nothing there to exclude. Adding their codes would
604
+ * be surface with no defect behind it.
605
+ */
606
+ codes: /* @__PURE__ */ new Set([
607
+ "42703",
608
+ // undefined_column
609
+ "42704",
610
+ // undefined_object — constraint, trigger, role, type, …
611
+ "3D000"
612
+ // invalid_catalog_name — `database "x" does not exist`
613
+ ]),
614
+ /**
615
+ * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
616
+ * failure about something *inside* a relation, which therefore says the
617
+ * relation itself is present. The two in-repo siblings that carry this
618
+ * phrase are `mapDataError` (`packages/rest`, #5352) and
619
+ * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
620
+ *
621
+ * [#6615] All three now read one home — `@objectstack/types` — instead
622
+ * of three hand-kept copies, so the phrase can no longer be taught to
623
+ * the repo a fourth time or drift in one package only. [#13279] This
624
+ * file now lives in that same home, so the read is a sibling import. The **width**
625
+ * difference that used to justify the copy is preserved and is the
626
+ * reason the home exports two functions rather than one: those two
627
+ * *extract* the column name to phrase a better error, so a miss costs a
628
+ * vaguer message; this one *excludes*, so a miss restores the
629
+ * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
630
+ * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
631
+ * anchors: any sub-object, any quoted identifier, any verdict.
632
+ * Over-matching here only ever converts a benign verdict into a loud
633
+ * one, which is the direction this whole module already errs in.
634
+ */
635
+ matchesMessage: isRelationSubObjectPhrase,
636
+ /**
637
+ * [#13324] "…and the relation it names is not the one you read."
638
+ *
639
+ * The sibling of the phrase above, reached one step further out. That
640
+ * one recognises a failure about something INSIDE a relation, which
641
+ * therefore says the relation is present; this one recognises a failure
642
+ * about a DIFFERENT relation, which says nothing at all about the one
643
+ * the caller read. Both end the question with `false` for the same
644
+ * reason: the licence this predicate grants — "there are no rows, so
645
+ * there is nothing to be inconsistent with" — is about the table that
646
+ * was READ, and neither phrase is evidence about it.
647
+ */
648
+ namesAnotherRelation: phraseNamesAnotherRelation
649
+ }
650
+ };
651
+ var MAX_CAUSE_DEPTH2 = 4;
652
+ var DRIVER_TARGETED_TABLE = /* @__PURE__ */ Symbol.for("objectstack.driver.targetedTable");
653
+ function declareTargetedTable(error, table) {
654
+ if (typeof table !== "string" || table === "") return error;
655
+ if (targetedTableOf(error) !== null) return error;
656
+ Object.defineProperty(error, DRIVER_TARGETED_TABLE, { value: table, enumerable: false });
657
+ return error;
658
+ }
659
+ function targetedTableOf(error) {
660
+ if (error === null || typeof error !== "object" && typeof error !== "function") return null;
661
+ const table = error[DRIVER_TARGETED_TABLE];
662
+ return typeof table === "string" && table !== "" ? table : null;
663
+ }
664
+ function excludedByReadObject(message, signature, relation) {
665
+ if (typeof relation !== "string" || relation === "") return false;
666
+ return signature.excludes?.namesAnotherRelation?.(message, relation) === true;
667
+ }
668
+ function matchesDriverError(error, signature, depth, readObject) {
669
+ if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH2) return false;
670
+ if (typeof error === "string") {
671
+ if (signature.excludes?.matchesMessage(error)) return false;
672
+ if (excludedByReadObject(error, signature, readObject)) return false;
673
+ return signature.message.test(error);
674
+ }
675
+ if (typeof error !== "object") return false;
676
+ const err = error;
677
+ const relation = targetedTableOf(err) ?? readObject;
678
+ const excludes = signature.excludes;
679
+ if (excludes) {
680
+ if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
681
+ if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
682
+ if (typeof err.message === "string" && excludedByReadObject(err.message, signature, relation))
683
+ return false;
684
+ }
685
+ if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
686
+ if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
687
+ if (typeof err.message === "string" && signature.message.test(err.message)) return true;
688
+ return matchesDriverError(err.cause, signature, depth + 1, relation);
689
+ }
690
+ function isSchemaAlreadyExistsError(error, depth = 0) {
691
+ return matchesDriverError(error, ALREADY_EXISTS, depth);
692
+ }
693
+ function isMissingTableError(error, readObject, depth = 0) {
694
+ return matchesDriverError(error, MISSING_TABLE, depth, readObject);
695
+ }
696
+
405
697
  // src/unbacked-conflict-target.ts
406
698
  var UNBACKED_CONFLICT_TARGET = {
407
699
  message: /ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint|there is no unique or exclusion constraint matching the ON CONFLICT specification/i
408
700
  };
409
- var MAX_CAUSE_DEPTH2 = 4;
701
+ var MAX_CAUSE_DEPTH3 = 4;
410
702
  function isUnbackedConflictTargetError(error) {
411
703
  return matchesUnbackedConflictTarget(error, 0);
412
704
  }
413
705
  function matchesUnbackedConflictTarget(error, depth) {
414
- if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH2) return false;
706
+ if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH3) return false;
415
707
  if (typeof error === "string") return UNBACKED_CONFLICT_TARGET.message.test(error);
416
708
  if (typeof error !== "object") return false;
417
709
  const err = error;
@@ -517,30 +809,40 @@ function postureGatesGlobalUniques(posture) {
517
809
  return normalizeTenancyPosture2(posture) === "isolated";
518
810
  }
519
811
  export {
812
+ DRIVER_TARGETED_TABLE,
520
813
  GLOBAL_UNIQUE_CONFIRMATION_REQUIRED,
521
814
  GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION,
522
815
  INTERNAL_ERROR_MESSAGE,
816
+ PLATFORM_OWNER_EMAIL_ENV,
817
+ SERVER_FAULT_LOG_PREFIX,
523
818
  VALIDATION_FAILED_STATUS,
524
819
  _resetEnvDeprecationWarnings,
525
820
  buildGlobalUniqueStopMessage,
526
821
  collectConfiguredLocales,
527
822
  collectGlobalUniques,
823
+ declareTargetedTable,
528
824
  declaredIndexUniqueIsGlobal,
529
825
  declaredUserMessage,
530
826
  declaresServerFault,
531
827
  demotedDeclaredCode,
828
+ describeFaultRequest,
532
829
  describeGlobalUniqueFinding,
533
830
  emitDegradedBootBanner,
534
831
  fieldUniqueIsGlobal,
535
832
  fieldsFromZodIssues,
536
833
  globalUniqueFindingId,
834
+ isEmailVerifiedUserRow,
537
835
  isMcpServerEnabled,
836
+ isMissingTableError,
538
837
  isModuleNotFoundError,
539
838
  isPlatformOwnedObject,
540
839
  isRelationSubObjectPhrase,
840
+ isSchemaAlreadyExistsError,
841
+ isServerFault,
541
842
  isUnbackedConflictTargetError,
542
843
  isUniqueViolationError,
543
844
  keysetWalk,
845
+ logServerFault,
544
846
  looksLikeInternalErrorLeak,
545
847
  matchMissingColumnOfRelation,
546
848
  postureGatesGlobalUniques,
@@ -552,13 +854,21 @@ export {
552
854
  resolveMcpStdioAutoStart,
553
855
  resolveMultiOrgEnabled,
554
856
  resolveOrgLimit,
857
+ resolveOrgMembershipLimit,
858
+ resolvePlatformOwnerEmail,
555
859
  resolveSandboxTimeoutMs,
556
860
  resolveSearchPinyinEnabled,
557
861
  resolveTenancyPosture,
558
862
  resolveThrownHttpError,
559
863
  sendError,
560
864
  sendOk,
865
+ serverFaultLogMessage,
866
+ serverFaultLogMeta,
867
+ serverFaultProvenance,
561
868
  stampSearchPinyinEnabled,
869
+ strandedDecisionDetails,
870
+ strandedDecisionFailure,
871
+ targetedTableOf,
562
872
  unconfirmedGlobalUniques,
563
873
  uniqueViolationColumn,
564
874
  validationFailure,