@vibeorm/schema 5.0.0 → 5.2.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.
@@ -90,6 +90,14 @@ export type DialectCapabilities = {
90
90
  readonly partialIndexes: boolean;
91
91
  /** Named-constraint conflict targets (`ON CONFLICT ON CONSTRAINT …`). */
92
92
  readonly conflictTargetConstraint: boolean;
93
+ /**
94
+ * A guard on the conflict's update arm: `ON CONFLICT (…) DO UPDATE SET …
95
+ * WHERE <guard>` — the existing row is updated (and returned) only when the
96
+ * guard admits it, in the same atomic statement. Tenant-scoped (`$scoped`)
97
+ * and `$withPolicy` upserts use it; MySQL's `ON DUPLICATE KEY UPDATE` has no
98
+ * WHERE, so there they keep the transactional find-then-write path.
99
+ */
100
+ readonly conflictUpdateWhere: boolean;
93
101
  /** DDL participates in transactions (mysql auto-commits DDL). */
94
102
  readonly transactionalDdl: boolean;
95
103
  /** createManyAndReturn support (requires multi-row RETURNING). */
@@ -134,6 +142,13 @@ export type DialectCapabilities = {
134
142
  readonly uuidColumns: "native" | "text" | "unsupported";
135
143
  /** String-backed Gregorian calendar days: native DATE, or TEXT with an owned CHECK on SQLite. */
136
144
  readonly localDateColumns: boolean;
145
+ /**
146
+ * Calendar truncation of a timestamp in a named time zone — the `groupBy`
147
+ * key `{ field, truncate, timeZone }` (PostgreSQL `date_trunc` with a zone,
148
+ * PostgreSQL 12 or later). Without it the key is refused when the query is
149
+ * built.
150
+ */
151
+ readonly dateTruncation: boolean;
137
152
  };
138
153
  export declare const POSTGRES_CAPABILITIES: DialectCapabilities;
139
154
  export declare const SQLITE_CAPABILITIES: DialectCapabilities;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Calendar truncation of a timestamp — the `groupBy` key form
3
+ * `{ field, truncate, timeZone }` (one group per hour, day, ISO week, month
4
+ * or year of a `DateTime` field, in a named time zone).
5
+ *
6
+ * ONE rule decides which fields can take it: the runtime's refusal, the
7
+ * renderer's expression and the generator's `by` type all call
8
+ * {@link dateTruncationStorage}, so they can never disagree.
9
+ */
10
+ import type { FieldType } from "./ir.ts";
11
+ /** A calendar period a timestamp can be truncated to (PostgreSQL `date_trunc` units; `week` is the ISO week, starting on Monday). */
12
+ export type DateTruncationUnit = "hour" | "day" | "week" | "month" | "year";
13
+ /** Every {@link DateTruncationUnit}, in increasing length. */
14
+ export declare const DATE_TRUNCATION_UNITS: readonly DateTruncationUnit[];
15
+ /**
16
+ * How a truncatable field stores its value on PostgreSQL. `"timestamptz"`: an
17
+ * instant (the canonical `DateTime` mapping, `@db.Timestamptz(n)`).
18
+ * `"timestamp"`: a zone-less column (`@db.Timestamp(n)`), which by VibeORM's
19
+ * contract holds the UTC wall-clock of the instant on every postgres driver.
20
+ */
21
+ export type DateTruncationStorage = "timestamptz" | "timestamp";
22
+ /**
23
+ * The storage form of a field a timestamp truncation can read, or `undefined`
24
+ * when the field cannot be truncated: not a `DateTime`, a list, an
25
+ * unsupported type, or a native type that is not a timestamp (`@db.Date`,
26
+ * `@db.Time`, `@db.Timetz`). A `DateTime` without a native type takes the
27
+ * canonical mapping, `timestamptz`.
28
+ */
29
+ export declare function dateTruncationStorage(params: {
30
+ readonly field: {
31
+ readonly type: FieldType;
32
+ readonly nativeType?: string;
33
+ readonly isList: boolean;
34
+ readonly isUnsupported?: boolean;
35
+ };
36
+ }): DateTruncationStorage | undefined;
37
+ //# sourceMappingURL=date-truncation.d.ts.map
package/dist/errors.d.ts CHANGED
@@ -130,8 +130,11 @@ export declare function internalError(params: {
130
130
  * Stable lifecycle categories; metadata never includes identity values.
131
131
  * `"null-claim"`: a key listed in `nativeRls.refuseNull` is bound to `null` and
132
132
  * the operation reaches a model whose rule reads it (or runs raw SQL).
133
+ * `"context-switched"`: the handle's transaction is running a `tx.$inContext`
134
+ * switch under another identity; the handle (and every handle derived from it)
135
+ * is refused until the switch has restored its claims.
133
136
  */
134
- export declare const RLS_CONTEXT_REASONS: readonly ["missing", "invalid", "expired", "conflicting", "null-claim"];
137
+ export declare const RLS_CONTEXT_REASONS: readonly ["missing", "invalid", "expired", "conflicting", "null-claim", "context-switched"];
135
138
  export type RlsContextReason = (typeof RLS_CONTEXT_REASONS)[number];
136
139
  /** Native RLS binding/lifecycle failure, distinct from database permissions. */
137
140
  export declare class RlsContextError extends VibeError {
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export { assertMigrationObjects, isSqlObjectDefinition, isSqlObjectIdentity, sql
26
26
  export type { MigrationObjectIR, QualifiedSqlName, SqlObjectDefinition, SqlObjectDefinitionFormat, SqlObjectFacts, SqlObjectIdentity } from "./migration-objects.ts";
27
27
  export type { AdvisoryLockMode, DialectCapabilities, FullTextEngine, RowLockStrength, RowLockWaitPolicy, TimeOfDayType, } from "./capabilities.ts";
28
28
  export { timeOfDayType } from "./time-of-day.ts";
29
+ export { DATE_TRUNCATION_UNITS, dateTruncationStorage } from "./date-truncation.ts";
30
+ export type { DateTruncationStorage, DateTruncationUnit } from "./date-truncation.ts";
29
31
  export { isLocalDateField, isCanonicalLocalDate, localDateCheckName } from "./local-date.ts";
30
32
  export { READ_ONLY_REFUSAL_REASONS, ReadOnlyClientError, SCOPED_REFUSAL_REASONS, ScopedQueryError, VIBE_ERROR_CODES, VibeError, internalError, invalidStoredData, unsupportedCapability, } from "./errors.ts";
31
33
  export type { ReadOnlyRefusalReason, ScopedRefusalReason, VibeErrorCode } from "./errors.ts";
package/dist/index.js CHANGED
@@ -71,7 +71,8 @@ var RLS_CONTEXT_REASONS = Object.freeze([
71
71
  "invalid",
72
72
  "expired",
73
73
  "conflicting",
74
- "null-claim"
74
+ "null-claim",
75
+ "context-switched"
75
76
  ]);
76
77
 
77
78
  class RlsContextError extends VibeError {
@@ -447,6 +448,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
447
448
  distinctOn: true,
448
449
  partialIndexes: true,
449
450
  conflictTargetConstraint: true,
451
+ conflictUpdateWhere: true,
450
452
  transactionalDdl: true,
451
453
  createManyAndReturn: true,
452
454
  jsonColumnType: true,
@@ -457,7 +459,8 @@ var POSTGRES_CAPABILITIES = Object.freeze({
457
459
  advisoryLocks: true,
458
460
  timeOfDayTypes: Object.freeze(["time", "timetz"]),
459
461
  uuidColumns: "native",
460
- localDateColumns: true
462
+ localDateColumns: true,
463
+ dateTruncation: true
461
464
  });
462
465
  var SQLITE_CAPABILITIES = Object.freeze({
463
466
  rowRuleAppendOnly: true,
@@ -485,6 +488,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
485
488
  distinctOn: false,
486
489
  partialIndexes: true,
487
490
  conflictTargetConstraint: false,
491
+ conflictUpdateWhere: true,
488
492
  transactionalDdl: true,
489
493
  createManyAndReturn: true,
490
494
  jsonColumnType: false,
@@ -495,7 +499,8 @@ var SQLITE_CAPABILITIES = Object.freeze({
495
499
  advisoryLocks: false,
496
500
  timeOfDayTypes: Object.freeze([]),
497
501
  uuidColumns: "text",
498
- localDateColumns: true
502
+ localDateColumns: true,
503
+ dateTruncation: false
499
504
  });
500
505
  var MYSQL_CAPABILITIES = Object.freeze({
501
506
  rowRuleAppendOnly: true,
@@ -523,6 +528,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
523
528
  distinctOn: false,
524
529
  partialIndexes: false,
525
530
  conflictTargetConstraint: false,
531
+ conflictUpdateWhere: false,
526
532
  transactionalDdl: false,
527
533
  createManyAndReturn: false,
528
534
  jsonColumnType: true,
@@ -533,7 +539,8 @@ var MYSQL_CAPABILITIES = Object.freeze({
533
539
  advisoryLocks: false,
534
540
  timeOfDayTypes: Object.freeze(["time"]),
535
541
  uuidColumns: "unsupported",
536
- localDateColumns: true
542
+ localDateColumns: true,
543
+ dateTruncation: false
537
544
  });
538
545
  var DIALECT_CAPABILITIES = Object.freeze({
539
546
  postgres: POSTGRES_CAPABILITIES,
@@ -1936,6 +1943,11 @@ function validateRowRules(params) {
1936
1943
  function wellFormedName(params) {
1937
1944
  return [...params.name].every((character) => character.length !== 1 || character.charCodeAt(0) < 55296 || character.charCodeAt(0) > 57343);
1938
1945
  }
1946
+ var REPEATED_RULE_ADVICE = Object.freeze({
1947
+ appendOnly: "Keep one: it already refuses every UPDATE and DELETE.",
1948
+ rowFixed: "Merge them into one rule whose except list holds only the fields both leave mutable.",
1949
+ frozen: "Merge them into one rule, or protect the fields of a later stage with a fixed rule gated by onceSet."
1950
+ });
1939
1951
  function validateRowRulesWithLookup(params) {
1940
1952
  const { model, dialect } = params;
1941
1953
  const rules = model.rowRules;
@@ -1947,7 +1959,7 @@ function validateRowRulesWithLookup(params) {
1947
1959
  const add = (params) => {
1948
1960
  issues.push({ rule: params.rule ?? "row-rule-invalid", model: model.name, ...params });
1949
1961
  };
1950
- const seenKinds = new Set;
1962
+ const firstOfKind = new Map;
1951
1963
  const seenDefinitions = new Set;
1952
1964
  const seenNames = new Set;
1953
1965
  for (const value of rules) {
@@ -1958,18 +1970,22 @@ function validateRowRulesWithLookup(params) {
1958
1970
  }
1959
1971
  const rule = value;
1960
1972
  const generated = rule.kind === "appendOnly" ? new Set : params.generatedFields({ model });
1961
- if (seenNames.has(rule.name))
1973
+ const first = rule.kind === "fixed" ? undefined : firstOfKind.get(rule.kind);
1974
+ if (first !== undefined && rule.kind !== "fixed")
1975
+ add({ message: `Model "${model.name}" declares more than one ${rule.kind} row rule (${first}, ${rule.name}). A model takes at most one ${rule.kind} rule; only fixed rules may repeat. ${REPEATED_RULE_ADVICE[rule.kind]}` });
1976
+ else if (seenNames.has(rule.name))
1962
1977
  add({ rule: "row-rule-name-collision", message: `Row rules share the physical stem ${rule.name}; supply an explicit, distinct name stem` });
1963
1978
  seenNames.add(rule.name);
1964
1979
  if (model.isView)
1965
1980
  add({ message: "Row rules require a stored table, not a view" });
1966
- if (rule.kind !== "fixed" && seenKinds.has(rule.kind))
1967
- add({ message: `Only one ${rule.kind} row rule is allowed per model` });
1968
- seenKinds.add(rule.kind);
1969
- const definition = rule.kind === "fixed" ? JSON.stringify([rule.kind, [...rule.columns].sort(), rule.when]) : rule.kind;
1970
- if (seenDefinitions.has(definition))
1971
- add({ message: `Duplicate ${rule.kind} row-rule declaration` });
1972
- seenDefinitions.add(definition);
1981
+ if (rule.kind !== "fixed" && first === undefined)
1982
+ firstOfKind.set(rule.kind, rule.name);
1983
+ if (rule.kind === "fixed") {
1984
+ const definition = JSON.stringify([rule.kind, [...rule.columns].sort(), rule.when]);
1985
+ if (seenDefinitions.has(definition))
1986
+ add({ message: "Duplicate fixed row-rule declaration" });
1987
+ seenDefinitions.add(definition);
1988
+ }
1973
1989
  const pair = rowRuleObjectNames({ rule });
1974
1990
  const names = dialect === "postgres" ? [pair.functionName, pair.triggerName] : (rule.kind === "appendOnly" ? ["update", "delete"] : ["update"]).map((event) => rowRuleTriggerName({ rule, event }));
1975
1991
  for (const name of names) {
@@ -2004,7 +2020,7 @@ function validateRowRulesWithLookup(params) {
2004
2020
  if (rule.kind !== "appendOnly" && (rule.kind === "fixed" && rule.columns.length === 0 || protectedFieldsWithGenerated({ model, rule, generated }).length === 0))
2005
2021
  add({ message: `${rule.kind} requires at least one protected stored field` });
2006
2022
  }
2007
- if (seenKinds.has("appendOnly") && rules.length > 1)
2023
+ if (firstOfKind.has("appendOnly") && rules.length > 1)
2008
2024
  add({ message: "appendOnly cannot combine with other row rules; it already refuses every UPDATE and DELETE" });
2009
2025
  return issues;
2010
2026
  }
@@ -2414,6 +2430,23 @@ function timeOfDayType(params) {
2414
2430
  return "timetz";
2415
2431
  return "time";
2416
2432
  }
2433
+ // packages/schema/src/date-truncation.ts
2434
+ var DATE_TRUNCATION_UNITS = Object.freeze(["hour", "day", "week", "month", "year"]);
2435
+ var ZONED_NATIVE_TYPE = /^timestamptz(?:\(\d+\))?$|^timestamp(?:\(\d+\))?withtimezone$/;
2436
+ var ZONE_LESS_NATIVE_TYPE = /^timestamp(?:\(\d+\))?(?:withouttimezone)?$/;
2437
+ function dateTruncationStorage(params) {
2438
+ const { field } = params;
2439
+ if (field.type !== "DateTime" || field.isList || field.isUnsupported === true)
2440
+ return;
2441
+ if (field.nativeType === undefined)
2442
+ return "timestamptz";
2443
+ const native = field.nativeType.toLowerCase().replace(/\s+/g, "");
2444
+ if (ZONED_NATIVE_TYPE.test(native))
2445
+ return "timestamptz";
2446
+ if (ZONE_LESS_NATIVE_TYPE.test(native))
2447
+ return "timestamp";
2448
+ return;
2449
+ }
2417
2450
  // packages/schema/src/computed.ts
2418
2451
  function buildComputedManifest(params) {
2419
2452
  const { computed } = params;
@@ -2778,6 +2811,8 @@ var RESERVED_ENUM_NAMES = [
2778
2811
  "KeysetPage",
2779
2812
  "KeysetOptions",
2780
2813
  "RowLockRequest",
2814
+ "GroupByTruncateUnit",
2815
+ "GroupByKeyName",
2781
2816
  "JsonValue",
2782
2817
  "InputJsonValue",
2783
2818
  "InputJsonArray",
@@ -2810,6 +2845,7 @@ var RESERVED_ENUM_NAMES = [
2810
2845
  "Skip",
2811
2846
  "TransactionOptions",
2812
2847
  "CallbackTransactionOptions",
2848
+ "RawStatement",
2813
2849
  "AdvisoryLockOptions",
2814
2850
  "StatementDiagnosticsOptions",
2815
2851
  "SchemaIR",
@@ -2849,6 +2885,11 @@ var GENERATED_TYPE_NAMES = {
2849
2885
  "ScalarOrderByInput",
2850
2886
  "RelationOrderByInput",
2851
2887
  "RelationFindManyArgs",
2888
+ "RelationAggregateOrderByInput",
2889
+ "RelationAvgOrderByInput",
2890
+ "RelationSumOrderByInput",
2891
+ "RelationMinOrderByInput",
2892
+ "RelationMaxOrderByInput",
2852
2893
  "CreateInput",
2853
2894
  "UncheckedCreateInput",
2854
2895
  "CreateManyInput",
@@ -2872,6 +2913,7 @@ var GENERATED_TYPE_NAMES = {
2872
2913
  "MaxAggregateOrderByInput",
2873
2914
  "HavingInput",
2874
2915
  "GroupByOrderByInput",
2916
+ "GroupByTruncatedKey",
2875
2917
  "FindManyArgs",
2876
2918
  "FindPageArgs",
2877
2919
  "FindFirstArgs",
@@ -3004,7 +3046,10 @@ function validateGeneratedNames(params) {
3004
3046
  owners.set("BoundVibeClientInstance", "native RLS client");
3005
3047
  owners.set("BoundVibeTransactionClientInstance", "native RLS client");
3006
3048
  owners.set("OperatorVibeClientInstance", "native RLS client");
3049
+ owners.set("OperatorVibeTransactionClientInstance", "native RLS client");
3007
3050
  }
3051
+ if (schema.models.some((model) => model.policy !== undefined))
3052
+ owners.set("PolicyClientInstance", "policy client");
3008
3053
  if (schema.models.some((model) => model.fields.some((field) => isLocalDateField({ field })))) {
3009
3054
  for (const name of ["LocalDate", "LocalDateFilter", "LocalDateNullableFilter"])
3010
3055
  owners.set(name, "local-date filters");
@@ -4368,6 +4413,7 @@ function validRlsForeignKey(params) {
4368
4413
  }
4369
4414
  export {
4370
4415
  CHECK_RULE_GROUP_MAX,
4416
+ DATE_TRUNCATION_UNITS,
4371
4417
  DIALECTS,
4372
4418
  DIALECT_CAPABILITIES,
4373
4419
  EMPTY_EXTENSION_REGISTRY,
@@ -4412,6 +4458,7 @@ export {
4412
4458
  buildComputedManifest,
4413
4459
  buildExtensionRegistry,
4414
4460
  checkRulePredicate,
4461
+ dateTruncationStorage,
4415
4462
  decodePrismaString,
4416
4463
  defaultExpressionIndexName,
4417
4464
  defaultForeignKeyName,
@@ -4502,4 +4549,4 @@ export {
4502
4549
  validatorRefIssue
4503
4550
  };
4504
4551
 
4505
- //# debugId=BE4A4B85E399756C64756E2164756E21
4552
+ //# debugId=6A81F5FFEA4B498F64756E2164756E21