@objectstack/types 17.2.0 → 17.3.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,7 +408,12 @@ 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
 
@@ -338,7 +429,7 @@ var RELATION_SUB_OBJECT = /["'`][^"'`]+["'`]\s+of relation\s/i;
338
429
 
339
430
  // src/unique-violation.ts
340
431
  var UNIQUE_VIOLATION = {
341
- codes: /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT_UNIQUE"]),
432
+ codes: /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY", "SQLITE_CONSTRAINT_UNIQUE", "UNIQUE_VIOLATION"]),
342
433
  errnos: /* @__PURE__ */ new Set([1062]),
343
434
  message: /unique constraint failed|violates unique constraint|unique violation|duplicate key|duplicate entry/i
344
435
  };
@@ -402,16 +493,199 @@ function uniqueViolationColumn(error) {
402
493
  return findUniqueViolationColumn(error, 0);
403
494
  }
404
495
 
496
+ // src/driver-error-classification.ts
497
+ var RELATION_IN_PHRASE = [
498
+ // SQLite / libsql: `no such table: sys_metadata_history`, and the
499
+ // schema-qualified `no such table: main.orders` it uses when it resolved
500
+ // the name itself (views, triggers) or the caller qualified it.
501
+ /no such table:\s*([^\s'"`;,()]+)/i,
502
+ // PostgreSQL: `relation "sys_metadata_history" does not exist`
503
+ /relation\s+["'`]([^"'`]+)["'`]\s+does not exist/i,
504
+ // MySQL / MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
505
+ /table\s+["'`]([^"'`]+)["'`]\s+doesn'?t exist/i,
506
+ // MySQL / MariaDB: `Unknown table 'app.t'`
507
+ /unknown table\s+["'`]([^"'`]+)["'`]/i
508
+ ];
509
+ function normaliseRelationName(name) {
510
+ const afterQualifier = name.slice(name.lastIndexOf(".") + 1);
511
+ const namespaceEnd = afterQualifier.lastIndexOf("__");
512
+ const bare = namespaceEnd === -1 ? afterQualifier : afterQualifier.slice(namespaceEnd + 2);
513
+ return bare.toLowerCase();
514
+ }
515
+ function phraseNamesAnotherRelation(message, readObject) {
516
+ const expected = normaliseRelationName(readObject);
517
+ if (expected === "") return false;
518
+ let named = false;
519
+ for (const pattern of RELATION_IN_PHRASE) {
520
+ const captured = pattern.exec(message)?.[1];
521
+ if (captured === void 0) continue;
522
+ const candidate = normaliseRelationName(captured);
523
+ if (candidate === "") continue;
524
+ if (candidate === expected) return false;
525
+ named = true;
526
+ }
527
+ return named;
528
+ }
529
+ var ALREADY_EXISTS = {
530
+ codes: /* @__PURE__ */ new Set([
531
+ // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
532
+ "42P07",
533
+ // duplicate_table
534
+ "42701",
535
+ // duplicate_column
536
+ "42710",
537
+ // duplicate_object — index / constraint already exists
538
+ // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
539
+ "ER_TABLE_EXISTS_ERROR",
540
+ // 1050
541
+ "ER_DUP_FIELDNAME",
542
+ // 1060
543
+ "ER_DUP_KEYNAME"
544
+ // 1061
545
+ ]),
546
+ errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
547
+ /**
548
+ * Message fallback for drivers that carry no machine-readable code —
549
+ * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
550
+ * every DDL failure, so the message is the only signal available:
551
+ * - `table sys_metadata already exists`
552
+ * - `duplicate column name: environment_id`
553
+ * - `index idx_x already exists`
554
+ * Postgres phrases its own as `relation "x" already exists` /
555
+ * `column "x" of relation "y" already exists`, which matches the same test.
556
+ */
557
+ message: /already exists|duplicate column name|duplicate key name/i
558
+ };
559
+ var MISSING_TABLE = {
560
+ codes: /* @__PURE__ */ new Set([
561
+ "42P01",
562
+ // PostgreSQL undefined_table
563
+ "ER_NO_SUCH_TABLE"
564
+ // MySQL / MariaDB 1146
565
+ ]),
566
+ errnos: /* @__PURE__ */ new Set([1146]),
567
+ /**
568
+ * - SQLite / libsql: `no such table: sys_metadata_history`
569
+ * - PostgreSQL: `relation "sys_metadata_history" does not exist`
570
+ * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
571
+ */
572
+ message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
573
+ excludes: {
574
+ /**
575
+ * Exactly the three SQLSTATEs the docblock above already names as
576
+ * must-stay-loud neighbours of `does not exist`. They are listed here
577
+ * rather than merely trusted to miss the message test, because two of
578
+ * them (42703 columns, 42704 constraints/triggers) have a phrasing that
579
+ * *does* hit it, and because a code is a fact where prose is a guess.
580
+ *
581
+ * Postgres-shaped on purpose: measured, neither MySQL
582
+ * (`Unknown column 'label' in 'field list'`) nor SQLite
583
+ * (`no such column: bogus`, `table t has no column named label`)
584
+ * phrases a sub-object failure so that a missing-table phrase falls out
585
+ * of it, so there is nothing there to exclude. Adding their codes would
586
+ * be surface with no defect behind it.
587
+ */
588
+ codes: /* @__PURE__ */ new Set([
589
+ "42703",
590
+ // undefined_column
591
+ "42704",
592
+ // undefined_object — constraint, trigger, role, type, …
593
+ "3D000"
594
+ // invalid_catalog_name — `database "x" does not exist`
595
+ ]),
596
+ /**
597
+ * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
598
+ * failure about something *inside* a relation, which therefore says the
599
+ * relation itself is present. The two in-repo siblings that carry this
600
+ * phrase are `mapDataError` (`packages/rest`, #5352) and
601
+ * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
602
+ *
603
+ * [#6615] All three now read one home — `@objectstack/types` — instead
604
+ * of three hand-kept copies, so the phrase can no longer be taught to
605
+ * the repo a fourth time or drift in one package only. [#13279] This
606
+ * file now lives in that same home, so the read is a sibling import. The **width**
607
+ * difference that used to justify the copy is preserved and is the
608
+ * reason the home exports two functions rather than one: those two
609
+ * *extract* the column name to phrase a better error, so a miss costs a
610
+ * vaguer message; this one *excludes*, so a miss restores the
611
+ * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
612
+ * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
613
+ * anchors: any sub-object, any quoted identifier, any verdict.
614
+ * Over-matching here only ever converts a benign verdict into a loud
615
+ * one, which is the direction this whole module already errs in.
616
+ */
617
+ matchesMessage: isRelationSubObjectPhrase,
618
+ /**
619
+ * [#13324] "…and the relation it names is not the one you read."
620
+ *
621
+ * The sibling of the phrase above, reached one step further out. That
622
+ * one recognises a failure about something INSIDE a relation, which
623
+ * therefore says the relation is present; this one recognises a failure
624
+ * about a DIFFERENT relation, which says nothing at all about the one
625
+ * the caller read. Both end the question with `false` for the same
626
+ * reason: the licence this predicate grants — "there are no rows, so
627
+ * there is nothing to be inconsistent with" — is about the table that
628
+ * was READ, and neither phrase is evidence about it.
629
+ */
630
+ namesAnotherRelation: phraseNamesAnotherRelation
631
+ }
632
+ };
633
+ var MAX_CAUSE_DEPTH2 = 4;
634
+ var DRIVER_TARGETED_TABLE = /* @__PURE__ */ Symbol.for("objectstack.driver.targetedTable");
635
+ function declareTargetedTable(error, table) {
636
+ if (typeof table !== "string" || table === "") return error;
637
+ if (targetedTableOf(error) !== null) return error;
638
+ Object.defineProperty(error, DRIVER_TARGETED_TABLE, { value: table, enumerable: false });
639
+ return error;
640
+ }
641
+ function targetedTableOf(error) {
642
+ if (error === null || typeof error !== "object" && typeof error !== "function") return null;
643
+ const table = error[DRIVER_TARGETED_TABLE];
644
+ return typeof table === "string" && table !== "" ? table : null;
645
+ }
646
+ function excludedByReadObject(message, signature, relation) {
647
+ if (typeof relation !== "string" || relation === "") return false;
648
+ return signature.excludes?.namesAnotherRelation?.(message, relation) === true;
649
+ }
650
+ function matchesDriverError(error, signature, depth, readObject) {
651
+ if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH2) return false;
652
+ if (typeof error === "string") {
653
+ if (signature.excludes?.matchesMessage(error)) return false;
654
+ if (excludedByReadObject(error, signature, readObject)) return false;
655
+ return signature.message.test(error);
656
+ }
657
+ if (typeof error !== "object") return false;
658
+ const err = error;
659
+ const relation = targetedTableOf(err) ?? readObject;
660
+ const excludes = signature.excludes;
661
+ if (excludes) {
662
+ if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
663
+ if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
664
+ if (typeof err.message === "string" && excludedByReadObject(err.message, signature, relation))
665
+ return false;
666
+ }
667
+ if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
668
+ if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
669
+ if (typeof err.message === "string" && signature.message.test(err.message)) return true;
670
+ return matchesDriverError(err.cause, signature, depth + 1, relation);
671
+ }
672
+ function isSchemaAlreadyExistsError(error, depth = 0) {
673
+ return matchesDriverError(error, ALREADY_EXISTS, depth);
674
+ }
675
+ function isMissingTableError(error, readObject, depth = 0) {
676
+ return matchesDriverError(error, MISSING_TABLE, depth, readObject);
677
+ }
678
+
405
679
  // src/unbacked-conflict-target.ts
406
680
  var UNBACKED_CONFLICT_TARGET = {
407
681
  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
682
  };
409
- var MAX_CAUSE_DEPTH2 = 4;
683
+ var MAX_CAUSE_DEPTH3 = 4;
410
684
  function isUnbackedConflictTargetError(error) {
411
685
  return matchesUnbackedConflictTarget(error, 0);
412
686
  }
413
687
  function matchesUnbackedConflictTarget(error, depth) {
414
- if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH2) return false;
688
+ if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH3) return false;
415
689
  if (typeof error === "string") return UNBACKED_CONFLICT_TARGET.message.test(error);
416
690
  if (typeof error !== "object") return false;
417
691
  const err = error;
@@ -517,30 +791,40 @@ function postureGatesGlobalUniques(posture) {
517
791
  return normalizeTenancyPosture2(posture) === "isolated";
518
792
  }
519
793
  export {
794
+ DRIVER_TARGETED_TABLE,
520
795
  GLOBAL_UNIQUE_CONFIRMATION_REQUIRED,
521
796
  GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION,
522
797
  INTERNAL_ERROR_MESSAGE,
798
+ PLATFORM_OWNER_EMAIL_ENV,
799
+ SERVER_FAULT_LOG_PREFIX,
523
800
  VALIDATION_FAILED_STATUS,
524
801
  _resetEnvDeprecationWarnings,
525
802
  buildGlobalUniqueStopMessage,
526
803
  collectConfiguredLocales,
527
804
  collectGlobalUniques,
805
+ declareTargetedTable,
528
806
  declaredIndexUniqueIsGlobal,
529
807
  declaredUserMessage,
530
808
  declaresServerFault,
531
809
  demotedDeclaredCode,
810
+ describeFaultRequest,
532
811
  describeGlobalUniqueFinding,
533
812
  emitDegradedBootBanner,
534
813
  fieldUniqueIsGlobal,
535
814
  fieldsFromZodIssues,
536
815
  globalUniqueFindingId,
816
+ isEmailVerifiedUserRow,
537
817
  isMcpServerEnabled,
818
+ isMissingTableError,
538
819
  isModuleNotFoundError,
539
820
  isPlatformOwnedObject,
540
821
  isRelationSubObjectPhrase,
822
+ isSchemaAlreadyExistsError,
823
+ isServerFault,
541
824
  isUnbackedConflictTargetError,
542
825
  isUniqueViolationError,
543
826
  keysetWalk,
827
+ logServerFault,
544
828
  looksLikeInternalErrorLeak,
545
829
  matchMissingColumnOfRelation,
546
830
  postureGatesGlobalUniques,
@@ -552,13 +836,19 @@ export {
552
836
  resolveMcpStdioAutoStart,
553
837
  resolveMultiOrgEnabled,
554
838
  resolveOrgLimit,
839
+ resolveOrgMembershipLimit,
840
+ resolvePlatformOwnerEmail,
555
841
  resolveSandboxTimeoutMs,
556
842
  resolveSearchPinyinEnabled,
557
843
  resolveTenancyPosture,
558
844
  resolveThrownHttpError,
559
845
  sendError,
560
846
  sendOk,
847
+ serverFaultLogMessage,
848
+ serverFaultLogMeta,
849
+ serverFaultProvenance,
561
850
  stampSearchPinyinEnabled,
851
+ targetedTableOf,
562
852
  unconfirmedGlobalUniques,
563
853
  uniqueViolationColumn,
564
854
  validationFailure,