@vibeorm/runtime 2.0.0-alpha.6 → 2.0.0-alpha.8

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.js CHANGED
@@ -133,6 +133,7 @@ function buildFieldMeta(params) {
133
133
  return {
134
134
  name: field.name,
135
135
  columnName: field.dbName ?? field.name,
136
+ ...field.collation === undefined ? {} : { collation: field.collation },
136
137
  type: field.type,
137
138
  scalarType: enumRef ? null : field.type,
138
139
  enumName: enumRef ? field.type.enum : null,
@@ -191,6 +192,11 @@ function buildModelMeta(params) {
191
192
  };
192
193
  if (primaryKey.length > 1) {
193
194
  addCompound(primaryKey.map((field) => field.name), false, model.primaryKey?.clientName);
195
+ } else {
196
+ const only = primaryKey[0];
197
+ if (only !== undefined && !only.isId && !only.isUnique) {
198
+ singleIndexUniques.push({ fields: [only], nullsNotDistinct: false });
199
+ }
194
200
  }
195
201
  for (const index of model.indexes) {
196
202
  if (index.kind !== "unique" || index.where !== undefined)
@@ -226,6 +232,7 @@ function buildModelMeta(params) {
226
232
  singleIndexUniques,
227
233
  relations: model.relations,
228
234
  relationByName: new Map(model.relations.map((relation) => [relation.name, relation])),
235
+ ...model.policy === undefined ? {} : { policy: model.policy },
229
236
  isView: model.isView,
230
237
  computed
231
238
  };
@@ -1435,6 +1442,7 @@ function compileOperatorFilter(params) {
1435
1442
  }
1436
1443
  const insensitive = mode === "insensitive";
1437
1444
  const strict = mode === "strict";
1445
+ const ciCollated = (params2) => params2.dialect.name === "mysql" && params2.field.collation?.toLowerCase().endsWith("_ci") === true;
1438
1446
  const requireStringOperand = (operator, operand) => {
1439
1447
  if (typeof operand !== "string") {
1440
1448
  throw validationError2({
@@ -1462,7 +1470,8 @@ function compileOperatorFilter(params) {
1462
1470
  kind: "like",
1463
1471
  column,
1464
1472
  pattern: escapeLikePattern({ raw: requireStringOperand("equals", operand) }),
1465
- insensitive: true
1473
+ insensitive: true,
1474
+ ...ciCollated({ dialect, field }) ? { ciCollation: true } : {}
1466
1475
  });
1467
1476
  break;
1468
1477
  }
@@ -1559,7 +1568,8 @@ function compileOperatorFilter(params) {
1559
1568
  kind: "like",
1560
1569
  column,
1561
1570
  pattern: compileStringPattern({ operator, operand, field, model }),
1562
- insensitive
1571
+ insensitive,
1572
+ ...insensitive && ciCollated({ dialect, field }) ? { ciCollation: true } : {}
1563
1573
  });
1564
1574
  break;
1565
1575
  }
@@ -5500,8 +5510,341 @@ function attachDefineView(params) {
5500
5510
  const value = (options) => createView({ meta, models, name: options.name, definition: options.definition });
5501
5511
  Object.defineProperty(client, "$defineView", { value, enumerable: true });
5502
5512
  }
5513
+ // packages/runtime/src/validators.ts
5514
+ import { VibeError as VibeError11 } from "@vibeorm/schema";
5515
+ function validationFailed(params) {
5516
+ return new VibeError11({
5517
+ code: "VIBE_VALIDATION",
5518
+ message: `validation failed for ${params.model}.${params.field}: ${params.issues[0]?.message ?? "invalid value"}`,
5519
+ meta: { model: params.model, field: params.field, issues: params.issues }
5520
+ });
5521
+ }
5522
+ function validateValue(params) {
5523
+ const result = params.schema["~standard"].validate(params.value);
5524
+ if (result instanceof Promise) {
5525
+ throw new VibeError11({
5526
+ code: "VIBE_VALIDATION",
5527
+ message: `async validator on ${params.model}.${params.field} \u2014 write validators must be synchronous (validate before calling the client for async rules)`,
5528
+ meta: { model: params.model, field: params.field }
5529
+ });
5530
+ }
5531
+ if (result.issues !== undefined) {
5532
+ throw validationFailed({ model: params.model, field: params.field, issues: result.issues });
5533
+ }
5534
+ return result.value;
5535
+ }
5536
+ function validateWriteData(params) {
5537
+ const { meta, model, data, validators } = params;
5538
+ const fieldSchemas = validators[model.name];
5539
+ const out = { ...data };
5540
+ for (const [key, value] of Object.entries(data)) {
5541
+ if (value === undefined)
5542
+ continue;
5543
+ const schema = fieldSchemas?.[key];
5544
+ if (schema !== undefined) {
5545
+ const isOperator = typeof value === "object" && value !== null && !Array.isArray(value) && "set" in value;
5546
+ if (isOperator) {
5547
+ const inner = value.set;
5548
+ out[key] = { set: validateValue({ model: model.name, field: key, schema, value: inner }) };
5549
+ } else {
5550
+ out[key] = validateValue({ model: model.name, field: key, schema, value });
5551
+ }
5552
+ continue;
5553
+ }
5554
+ const relation = model.relationByName.get(key);
5555
+ if (relation === undefined || typeof value !== "object" || value === null)
5556
+ continue;
5557
+ const target = meta.modelByName.get(relation.targetModel);
5558
+ if (target === undefined || validators[target.name] === undefined)
5559
+ continue;
5560
+ const verbs = value;
5561
+ const recurse = (row) => typeof row === "object" && row !== null ? validateWriteData({ meta, model: target, data: row, validators }) : row;
5562
+ const next = { ...verbs };
5563
+ if (verbs.create !== undefined) {
5564
+ next.create = Array.isArray(verbs.create) ? verbs.create.map(recurse) : recurse(verbs.create);
5565
+ }
5566
+ if (verbs.connectOrCreate !== undefined) {
5567
+ const entries = Array.isArray(verbs.connectOrCreate) ? verbs.connectOrCreate : [verbs.connectOrCreate];
5568
+ const mapped = entries.map((entry) => {
5569
+ const pair = entry;
5570
+ return { ...pair, create: recurse(pair.create) };
5571
+ });
5572
+ next.connectOrCreate = Array.isArray(verbs.connectOrCreate) ? mapped : mapped[0];
5573
+ }
5574
+ if (verbs.update !== undefined && !Array.isArray(verbs.update)) {
5575
+ const body = verbs.update;
5576
+ next.update = body.data !== undefined ? { ...body, data: recurse(body.data) } : recurse(body);
5577
+ }
5578
+ if (verbs.upsert !== undefined && !Array.isArray(verbs.upsert)) {
5579
+ const body = verbs.upsert;
5580
+ next.upsert = { ...body, create: recurse(body.create), update: recurse(body.update) };
5581
+ }
5582
+ out[key] = next;
5583
+ }
5584
+ return out;
5585
+ }
5586
+ function applyWriteValidators(params) {
5587
+ const { meta, model, method, args, validators } = params;
5588
+ const validate = (row) => typeof row === "object" && row !== null ? validateWriteData({ meta, model, data: row, validators }) : row;
5589
+ if (method === "create" || method === "update" || method === "updateMany") {
5590
+ return args.data === undefined ? args : { ...args, data: validate(args.data) };
5591
+ }
5592
+ if (method === "createMany" || method === "createManyAndReturn") {
5593
+ return Array.isArray(args.data) ? { ...args, data: args.data.map(validate) } : args;
5594
+ }
5595
+ if (method === "upsert") {
5596
+ return { ...args, create: validate(args.create), update: validate(args.update) };
5597
+ }
5598
+ return args;
5599
+ }
5600
+ // packages/runtime/src/policy.ts
5601
+ import { VibeError as VibeError12 } from "@vibeorm/schema";
5602
+ function policyError(message, meta) {
5603
+ return new VibeError12({ code: "VIBE_VALIDATION", message, meta: meta ?? {} });
5604
+ }
5605
+ function policiedModels(meta) {
5606
+ return meta.models.filter((model) => model.policy !== undefined);
5607
+ }
5608
+ function mergeWhere(params) {
5609
+ const { where, field, value } = params;
5610
+ const predicate = { [field]: value };
5611
+ return where === undefined ? predicate : { AND: [where, predicate] };
5612
+ }
5613
+ function uniqueToFilter(params) {
5614
+ const out = {};
5615
+ for (const [key, value] of Object.entries(params.where)) {
5616
+ const isCompound = typeof value === "object" && value !== null && !Array.isArray(value) && params.model.compoundUniques.has(key);
5617
+ if (isCompound)
5618
+ Object.assign(out, value);
5619
+ else
5620
+ out[key] = value;
5621
+ }
5622
+ return out;
5623
+ }
5624
+ function scopeSelection(params) {
5625
+ const { meta, model, args, context } = params;
5626
+ const out = { ...args };
5627
+ for (const key of ["include", "select"]) {
5628
+ const block = args[key];
5629
+ if (typeof block !== "object" || block === null)
5630
+ continue;
5631
+ const next = { ...block };
5632
+ for (const [name, spec] of Object.entries(block)) {
5633
+ const relation = model.relationByName.get(name);
5634
+ if (relation === undefined)
5635
+ continue;
5636
+ const target = meta.modelByName.get(relation.targetModel);
5637
+ if (target?.policy === undefined)
5638
+ continue;
5639
+ const value = context[target.policy.context];
5640
+ const spec2 = spec === true ? {} : { ...spec };
5641
+ spec2.where = mergeWhere({ where: spec2.where, field: target.policy.field, value });
5642
+ next[name] = scopeSelection({ meta, model: target, args: spec2, context });
5643
+ }
5644
+ out[key] = next;
5645
+ }
5646
+ return out;
5647
+ }
5648
+ var REFERENCE_VERBS = ["connect", "connectOrCreate", "set", "disconnect", "update", "updateMany", "delete", "deleteMany", "upsert"];
5649
+ function forceCreateData(params) {
5650
+ const { meta, model, data, context } = params;
5651
+ const out = { ...data };
5652
+ if (model.policy !== undefined) {
5653
+ const value = context[model.policy.context];
5654
+ const given = data[model.policy.field];
5655
+ if (given !== undefined && given !== value) {
5656
+ throw policyError(`create on ${model.name}: data.${model.policy.field} conflicts with the bound policy context`, { model: model.name, field: model.policy.field });
5657
+ }
5658
+ out[model.policy.field] = value;
5659
+ }
5660
+ for (const [name, spec] of Object.entries(data)) {
5661
+ const relation = model.relationByName.get(name);
5662
+ if (relation === undefined || typeof spec !== "object" || spec === null)
5663
+ continue;
5664
+ const target = meta.modelByName.get(relation.targetModel);
5665
+ if (target?.policy === undefined)
5666
+ continue;
5667
+ const verbs = spec;
5668
+ for (const verb of REFERENCE_VERBS) {
5669
+ if (verbs[verb] !== undefined) {
5670
+ throw policyError(`nested ${verb} on policied model ${target.name} is not policy-safe from a scoped client \u2014 perform it in an explicit $transaction on the scoped delegate`, { model: model.name, relation: name, verb });
5671
+ }
5672
+ }
5673
+ if (verbs.create !== undefined) {
5674
+ const recurse = (row) => forceCreateData({ meta, model: target, data: row, context });
5675
+ out[name] = { ...verbs, create: Array.isArray(verbs.create) ? verbs.create.map(recurse) : recurse(verbs.create) };
5676
+ }
5677
+ }
5678
+ return out;
5679
+ }
5680
+ function scopedDelegate(params) {
5681
+ const { meta, model, base, context, transaction } = params;
5682
+ const policy = model.policy;
5683
+ const value = context[policy.context];
5684
+ const pk = model.primaryKey.map((field) => field.name);
5685
+ const scopeFind = (args) => {
5686
+ const scoped = scopeSelection({ meta, model, args: args ?? {}, context });
5687
+ return { ...scoped, where: mergeWhere({ where: scoped.where, field: policy.field, value }) };
5688
+ };
5689
+ const scopeUnique = (args) => {
5690
+ const scoped = scopeSelection({ meta, model, args, context });
5691
+ const flat = uniqueToFilter({ model, where: args.where ?? {} });
5692
+ return { ...scoped, where: mergeWhere({ where: flat, field: policy.field, value }) };
5693
+ };
5694
+ const pkClientKey = () => {
5695
+ for (const [key, group] of model.compoundUniques) {
5696
+ const names = group.fields.map((field) => field.name);
5697
+ if (names.length === pk.length && names.every((name) => pk.includes(name)))
5698
+ return key;
5699
+ }
5700
+ return pk.join("_");
5701
+ };
5702
+ const lockThen = (args, write) => transaction(async (tx) => {
5703
+ const raw = tx.$bypassPolicy === undefined ? tx : tx.$bypassPolicy();
5704
+ const target = await raw.$models[model.name].findFirst({
5705
+ where: scopeUnique(args).where,
5706
+ select: Object.fromEntries(pk.map((name) => [name, true])),
5707
+ lock: "update"
5708
+ });
5709
+ if (target === null)
5710
+ return null;
5711
+ const pkWhere = pk.length === 1 ? { [pk[0]]: target[pk[0]] } : { [pkClientKey()]: Object.fromEntries(pk.map((name) => [name, target[name]])) };
5712
+ return write(raw, pkWhere);
5713
+ });
5714
+ const notFound2 = (method) => new VibeError12({ code: "VIBE_NOT_FOUND", message: `${method} on ${model.name}: no row matches the unique where inside the bound policy context`, meta: { model: model.name, method } });
5715
+ return {
5716
+ findMany: (args) => base.findMany(scopeFind(args)),
5717
+ findFirst: (args) => base.findFirst(scopeFind(args)),
5718
+ findFirstOrThrow: (args) => base.findFirstOrThrow(scopeFind(args)),
5719
+ findUnique: (args) => base.findFirst(scopeUnique(args)),
5720
+ findUniqueOrThrow: (args) => base.findFirstOrThrow(scopeUnique(args)),
5721
+ create: async (args) => base.create({ ...args, data: forceCreateData({ meta, model, data: args.data ?? {}, context }) }),
5722
+ createMany: async (args) => base.createMany({
5723
+ ...args,
5724
+ data: (args.data ?? []).map((row) => forceCreateData({ meta, model, data: row, context }))
5725
+ }),
5726
+ createManyAndReturn: async (args) => base.createManyAndReturn({
5727
+ ...args,
5728
+ data: (args.data ?? []).map((row) => forceCreateData({ meta, model, data: row, context }))
5729
+ }),
5730
+ update: async (args) => {
5731
+ const row = await lockThen(args, (raw, pkWhere) => raw.$models[model.name].update({ ...args, where: pkWhere }));
5732
+ if (row === null)
5733
+ throw notFound2("update");
5734
+ return row;
5735
+ },
5736
+ updateMany: (args) => base.updateMany({ ...args, where: mergeWhere({ where: args.where, field: policy.field, value }) }),
5737
+ upsert: async (args) => {
5738
+ const row = await lockThen(args, (raw, pkWhere) => raw.$models[model.name].update({ where: pkWhere, data: args.update, ...args.select === undefined ? {} : { select: args.select } }));
5739
+ if (row !== null)
5740
+ return row;
5741
+ return await base.create({
5742
+ data: forceCreateData({ meta, model, data: args.create ?? {}, context }),
5743
+ ...args.select === undefined ? {} : { select: args.select }
5744
+ });
5745
+ },
5746
+ delete: async (args) => {
5747
+ const row = await lockThen(args, (raw, pkWhere) => raw.$models[model.name].delete({ ...args, where: pkWhere }));
5748
+ if (row === null)
5749
+ throw notFound2("delete");
5750
+ return row;
5751
+ },
5752
+ deleteMany: (args) => base.deleteMany({ ...args ?? {}, where: mergeWhere({ where: args?.where, field: policy.field, value }) }),
5753
+ count: (args) => base.count(scopeFind(args)),
5754
+ aggregate: (args) => base.aggregate(scopeFind(args)),
5755
+ groupBy: (args) => base.groupBy(scopeFind(args))
5756
+ };
5757
+ }
5758
+ function scopeExtensionProps(params) {
5759
+ const out = {};
5760
+ for (const [key, prop] of Object.entries(params.base)) {
5761
+ if (key in params.delegate)
5762
+ continue;
5763
+ if (typeof prop === "function") {
5764
+ out[key] = (args) => prop({ ...args ?? {}, where: mergeWhere({ where: args?.where, field: params.field, value: params.value }) });
5765
+ } else if (typeof prop === "object" && prop !== null) {
5766
+ out[key] = scopeExtensionProps({ base: prop, delegate: {}, field: params.field, value: params.value });
5767
+ }
5768
+ }
5769
+ return out;
5770
+ }
5771
+ function attachPolicy(params) {
5772
+ const { client, meta } = params;
5773
+ const policied = policiedModels(meta);
5774
+ if (policied.length === 0)
5775
+ return;
5776
+ const originals = new Map;
5777
+ for (const model of policied) {
5778
+ const original = client.$models[model.name];
5779
+ originals.set(model.name, { ...original });
5780
+ const guard = (method) => () => Promise.reject(policyError(`model ${model.name} declares @@policy \u2014 bind a context with $withPolicy({ ${model.policy?.context}: ... }) or bypass deliberately with $bypassPolicy()`, { model: model.name, method }));
5781
+ const guarded = {};
5782
+ for (const key of Object.keys(original)) {
5783
+ guarded[key] = typeof original[key] === "function" ? guard(key) : original[key];
5784
+ }
5785
+ Object.assign(original, guarded);
5786
+ }
5787
+ const buildScoped = (context) => {
5788
+ for (const model of policied) {
5789
+ const key = model.policy?.context;
5790
+ if (context[key] === undefined || context[key] === null) {
5791
+ throw policyError(`$withPolicy: missing context value "${key}" (required by ${model.name})`, { context: key });
5792
+ }
5793
+ }
5794
+ const scoped = Object.create(client);
5795
+ const scopedModels = { ...client.$models };
5796
+ for (const model of policied) {
5797
+ const base = originals.get(model.name);
5798
+ const value = context[model.policy?.context];
5799
+ const delegate = scopedDelegate({ meta, model, base, context, transaction: client.$transaction });
5800
+ const full = { ...delegate, ...scopeExtensionProps({ base, delegate, field: model.policy?.field, value }) };
5801
+ scopedModels[model.name] = full;
5802
+ scopedModels[model.clientName] = full;
5803
+ Object.defineProperty(scoped, model.clientName, { value: full, enumerable: true });
5804
+ }
5805
+ Object.defineProperty(scoped, "$models", { value: scopedModels, enumerable: true });
5806
+ attachDefineView({ client: scoped, meta, models: scopedModels });
5807
+ Object.defineProperty(scoped, "$transaction", {
5808
+ value: (arg, options) => typeof arg === "function" ? client.$transaction((tx) => arg(buildScopedOver(tx, context)), options) : client.$transaction(arg, options),
5809
+ enumerable: true
5810
+ });
5811
+ return scoped;
5812
+ };
5813
+ const buildScopedOver = (tx, context) => {
5814
+ const rawTx = tx.$bypassPolicy === undefined ? tx : tx.$bypassPolicy();
5815
+ const inner = Object.create(tx);
5816
+ const scopedModels = { ...tx.$models };
5817
+ for (const model of policied) {
5818
+ const base = rawTx.$models[model.name];
5819
+ const delegate = scopedDelegate({ meta, model, base, context, transaction: tx.$transaction });
5820
+ scopedModels[model.name] = delegate;
5821
+ scopedModels[model.clientName] = delegate;
5822
+ Object.defineProperty(inner, model.clientName, { value: delegate, enumerable: true });
5823
+ }
5824
+ Object.defineProperty(inner, "$models", { value: scopedModels, enumerable: true });
5825
+ attachDefineView({ client: inner, meta, models: scopedModels });
5826
+ return inner;
5827
+ };
5828
+ Object.defineProperty(client, "$withPolicy", { value: (context) => buildScoped(context), enumerable: true });
5829
+ Object.defineProperty(client, "$bypassPolicy", {
5830
+ value: () => {
5831
+ const bypass = Object.create(client);
5832
+ const models = { ...client.$models };
5833
+ for (const model of policied) {
5834
+ const original = originals.get(model.name);
5835
+ models[model.name] = original;
5836
+ models[model.clientName] = original;
5837
+ Object.defineProperty(bypass, model.clientName, { value: original, enumerable: true });
5838
+ }
5839
+ Object.defineProperty(bypass, "$models", { value: models, enumerable: true });
5840
+ attachDefineView({ client: bypass, meta, models });
5841
+ return bypass;
5842
+ },
5843
+ enumerable: true
5844
+ });
5845
+ }
5503
5846
  // packages/runtime/src/client.ts
5504
- import { VibeError as VibeError11, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5847
+ import { VibeError as VibeError13, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
5505
5848
  import { ParamCollector, getSqlDialect } from "@vibeorm/sql";
5506
5849
 
5507
5850
  // packages/runtime/src/render-cache.ts
@@ -5951,7 +6294,7 @@ function renderStatementCached(params) {
5951
6294
  // packages/runtime/src/client.ts
5952
6295
  function notFound2(params) {
5953
6296
  const { model, method } = params;
5954
- return new VibeError11({
6297
+ return new VibeError13({
5955
6298
  code: "VIBE_NOT_FOUND",
5956
6299
  message: `no ${model} record matched the where clause of ${method}()`,
5957
6300
  meta: { model, method }
@@ -5959,7 +6302,7 @@ function notFound2(params) {
5959
6302
  }
5960
6303
  function viewIsReadOnly(params) {
5961
6304
  const { model, method } = params;
5962
- return new VibeError11({
6305
+ return new VibeError13({
5963
6306
  code: "VIBE_VALIDATION",
5964
6307
  message: `${model} is a view \u2014 ${method}() is not available on views (they are read-only); write to the underlying table instead`,
5965
6308
  meta: { model, method }
@@ -6088,7 +6431,7 @@ class QueryPromise extends Promise {
6088
6431
  }
6089
6432
  claim() {
6090
6433
  if (this.started) {
6091
- throw new VibeError11({
6434
+ throw new VibeError13({
6092
6435
  code: "VIBE_VALIDATION",
6093
6436
  message: "$transaction([...]) needs operations that have not dispatched \u2014 pass delegate calls in the same tick they are created (this one was awaited or created earlier and already executed outside the transaction)"
6094
6437
  });
@@ -6166,7 +6509,7 @@ async function runAffected(params) {
6166
6509
  function plan(params) {
6167
6510
  const { engine, model, method, args } = params;
6168
6511
  if (args !== undefined && args["lock"] !== undefined && engine.inTransaction !== true) {
6169
- throw new VibeError11({
6512
+ throw new VibeError13({
6170
6513
  code: "VIBE_VALIDATION",
6171
6514
  message: `lock on ${model}.${method}() requires a transaction \u2014 call it on the client $transaction hands its callback (outside one, the lock would release at statement end)`,
6172
6515
  meta: { model, method }
@@ -6239,7 +6582,7 @@ async function insertedKeyWhere(params) {
6239
6582
  });
6240
6583
  const id = result.rows[0]?.id;
6241
6584
  if (id === undefined || id === null) {
6242
- throw new VibeError11({
6585
+ throw new VibeError13({
6243
6586
  code: "VIBE_INTERNAL",
6244
6587
  message: `could not read back the generated key for ${source.model.name}`,
6245
6588
  meta: { model: source.model.name }
@@ -6541,7 +6884,7 @@ function createDelegate(params) {
6541
6884
  const skipping = args.onConflict !== undefined;
6542
6885
  if (hasNestedWrites({ model: modelMeta, data: args.data })) {
6543
6886
  if (skipping) {
6544
- throw new VibeError11({
6887
+ throw new VibeError13({
6545
6888
  code: "VIBE_VALIDATION",
6546
6889
  message: `onConflict cannot combine with nested writes on ${modelMeta.name}.create() \u2014 insert the parent row alone, then connect relations`,
6547
6890
  meta: { model: modelMeta.name }
@@ -6625,7 +6968,7 @@ function createDelegate(params) {
6625
6968
  const upsert = async (target, args) => {
6626
6969
  const modelMeta = getModelMeta({ meta: target.meta, model });
6627
6970
  if (hasNestedWrites({ model: modelMeta, data: args.create }) || hasNestedWrites({ model: modelMeta, data: args.update })) {
6628
- throw new VibeError11({
6971
+ throw new VibeError13({
6629
6972
  code: "VIBE_VALIDATION",
6630
6973
  message: `nested writes are not supported inside upsert arms on ${modelMeta.name} \u2014 use create()/update() with nested data`,
6631
6974
  meta: { model: modelMeta.name }
@@ -6633,7 +6976,7 @@ function createDelegate(params) {
6633
6976
  }
6634
6977
  if (!upsertUsesNativeStatement({ model: modelMeta, args })) {
6635
6978
  if (args.onConflict !== undefined) {
6636
- throw new VibeError11({
6979
+ throw new VibeError13({
6637
6980
  code: "VIBE_VALIDATION",
6638
6981
  message: `onConflict on ${modelMeta.name}.upsert() needs the native single-statement path \u2014 this shape (a guard field beyond the conflict target, or create data not binding the where's key) runs the find-then-write fallback, which cannot honor an explicit conflict target`,
6639
6982
  meta: { model: modelMeta.name }
@@ -6760,6 +7103,17 @@ function createDelegate(params) {
6760
7103
  const meta = getModelMeta({ meta: engine.meta, model });
6761
7104
  return meta.isView ? readOnlyDelegate({ delegate, model: meta.name }) : delegate;
6762
7105
  }
7106
+ var WRITE_METHODS = ["create", "createMany", "createManyAndReturn", "update", "updateMany", "upsert"];
7107
+ function withWriteValidation(params) {
7108
+ const { engine, delegate, model, validators } = params;
7109
+ const meta = getModelMeta({ meta: engine.meta, model });
7110
+ const wrapped = { ...delegate };
7111
+ for (const method of WRITE_METHODS) {
7112
+ const original = delegate[method];
7113
+ wrapped[method] = (args) => original(applyWriteValidators({ meta: engine.meta, model: meta, method, args, validators }));
7114
+ }
7115
+ return wrapped;
7116
+ }
6763
7117
  function buildRawStatement(params) {
6764
7118
  const { dialect, strings, values } = params;
6765
7119
  const collector = new ParamCollector(dialect);
@@ -6773,7 +7127,11 @@ function buildClient(params) {
6773
7127
  const { engine } = params;
6774
7128
  const models = {};
6775
7129
  for (const model of engine.meta.models) {
6776
- const delegate = createDelegate({ engine, model: model.name });
7130
+ let delegate = createDelegate({ engine, model: model.name });
7131
+ const validators = engine.options?.validate;
7132
+ if (validators?.[model.name] !== undefined) {
7133
+ delegate = withWriteValidation({ engine, delegate, model: model.name, validators });
7134
+ }
6777
7135
  models[model.name] = delegate;
6778
7136
  models[model.clientName] = delegate;
6779
7137
  }
@@ -6795,7 +7153,7 @@ function buildClient(params) {
6795
7153
  let claimError = null;
6796
7154
  for (const operation of operations) {
6797
7155
  if (!(operation instanceof QueryPromise)) {
6798
- claimError = new VibeError11({
7156
+ claimError = new VibeError13({
6799
7157
  code: "VIBE_VALIDATION",
6800
7158
  message: "$transaction([...]) takes query promises from model delegate calls \u2014 a foreign promise is already running and cannot join the transaction"
6801
7159
  });
@@ -6834,7 +7192,7 @@ function buildClient(params) {
6834
7192
  $models: models,
6835
7193
  $on: (event, listener) => {
6836
7194
  if (event !== "query") {
6837
- throw new VibeError11({
7195
+ throw new VibeError13({
6838
7196
  code: "VIBE_VALIDATION",
6839
7197
  message: `unknown client event "${String(event)}" \u2014 supported: "query"`,
6840
7198
  meta: { event }
@@ -6868,6 +7226,7 @@ function buildClient(params) {
6868
7226
  }
6869
7227
  attachExtensionMounts({ engine, client, models });
6870
7228
  attachDefineView({ client, meta: engine.meta, models });
7229
+ attachPolicy({ client, meta: engine.meta });
6871
7230
  return client;
6872
7231
  }
6873
7232
  function makeMountContext(params) {
@@ -6897,7 +7256,7 @@ function makeMountContext(params) {
6897
7256
  compileWhere: ({ where, model: modelName, qualify }) => {
6898
7257
  const target = modelName === undefined ? model : getModelMeta({ meta: engine.meta, model: modelName });
6899
7258
  if (target === null) {
6900
- throw new VibeError11({
7259
+ throw new VibeError13({
6901
7260
  code: "VIBE_INTERNAL",
6902
7261
  message: `extension "${extension}" compiled a where with no model in scope`,
6903
7262
  meta: { extension }
@@ -6923,7 +7282,7 @@ function attachExtensionMounts(params) {
6923
7282
  const model = getModelMeta({ meta: engine.meta, model: mount.model ?? "" });
6924
7283
  const delegate = models[model.name];
6925
7284
  if (delegate === undefined || mount.property in delegate) {
6926
- throw new VibeError11({
7285
+ throw new VibeError13({
6927
7286
  code: "VIBE_CONFIG",
6928
7287
  message: `extension "${mount.extension}" cannot mount "${mount.property}" on ${model.name} \u2014 the name is taken`,
6929
7288
  meta: { extension: mount.extension, model: model.name, property: mount.property }
@@ -6934,7 +7293,7 @@ function attachExtensionMounts(params) {
6934
7293
  continue;
6935
7294
  }
6936
7295
  if (!mount.property.startsWith("$") || mount.property in client) {
6937
- throw new VibeError11({
7296
+ throw new VibeError13({
6938
7297
  code: "VIBE_CONFIG",
6939
7298
  message: `extension "${mount.extension}" cannot mount client property "${mount.property}" \u2014 root mounts are $-prefixed and must not collide`,
6940
7299
  meta: { extension: mount.extension, property: mount.property }
@@ -6954,7 +7313,7 @@ function createClient(params) {
6954
7313
  assertComputedManifest({ manifest: computed.manifest, specs: computed.specs });
6955
7314
  }
6956
7315
  if (computed === undefined && options !== undefined && options["computed"] !== undefined) {
6957
- throw new VibeError11({
7316
+ throw new VibeError13({
6958
7317
  code: "VIBE_CONFIG",
6959
7318
  message: "this client was generated without computed fields \u2014 declare them in vibeorm.config.ts and re-run vibeorm generate",
6960
7319
  meta: {}
@@ -6982,6 +7341,7 @@ function createClient(params) {
6982
7341
  });
6983
7342
  }
6984
7343
  export {
7344
+ validateWriteData,
6985
7345
  updateWithNested,
6986
7346
  toSqlExecutor,
6987
7347
  toClientName,
@@ -7017,10 +7377,12 @@ export {
7017
7377
  buildExtensionOperatorMap,
7018
7378
  buildConnectHooks,
7019
7379
  attachRelations,
7380
+ attachPolicy,
7020
7381
  attachLateralRelations,
7021
7382
  attachDefineView,
7022
7383
  assertExtensionManifest,
7023
7384
  assertComputedManifest,
7385
+ applyWriteValidators,
7024
7386
  applyComputedFields,
7025
7387
  ViewResult,
7026
7388
  SQLITE_CODECS,
@@ -7030,4 +7392,4 @@ export {
7030
7392
  COUNT_ALIAS
7031
7393
  };
7032
7394
 
7033
- //# debugId=4B6B11C516D4A81A64756E2164756E21
7395
+ //# debugId=EC61C00B9601D9AD64756E2164756E21