@vibeorm/runtime 2.0.0-alpha.8 → 2.0.0-alpha.9
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/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +955 -19
- package/dist/index.js.map +5 -4
- package/dist/scoped.d.ts +131 -0
- package/dist/scoped.d.ts.map +1 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -5843,8 +5843,939 @@ function attachPolicy(params) {
|
|
|
5843
5843
|
enumerable: true
|
|
5844
5844
|
});
|
|
5845
5845
|
}
|
|
5846
|
+
// packages/runtime/src/scoped.ts
|
|
5847
|
+
import { ScopedQueryError, VibeError as VibeError13 } from "@vibeorm/schema";
|
|
5848
|
+
function refusal(params) {
|
|
5849
|
+
return new ScopedQueryError(params);
|
|
5850
|
+
}
|
|
5851
|
+
function isPlainObject5(value) {
|
|
5852
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
|
|
5853
|
+
}
|
|
5854
|
+
var SCOPEABLE_COLUMN_TYPES = ["String", "Int", "BigInt"];
|
|
5855
|
+
var resolvedCache = new WeakMap;
|
|
5856
|
+
function nestedModeOf(params) {
|
|
5857
|
+
const raw = params.entry.nested;
|
|
5858
|
+
if (raw === undefined)
|
|
5859
|
+
return "verify";
|
|
5860
|
+
if (raw === "verify" || raw === "refuse")
|
|
5861
|
+
return raw;
|
|
5862
|
+
throw refusal({
|
|
5863
|
+
model: params.model,
|
|
5864
|
+
path: "models",
|
|
5865
|
+
reason: "invalid_entry",
|
|
5866
|
+
message: `$scoped: models.${params.model}.nested must be "verify" or "refuse", got ${String(raw)}`
|
|
5867
|
+
});
|
|
5868
|
+
}
|
|
5869
|
+
function resolveScopedModels(params) {
|
|
5870
|
+
const { meta, models } = params;
|
|
5871
|
+
const byMap = resolvedCache.get(models) ?? new WeakMap;
|
|
5872
|
+
const cached = byMap.get(meta);
|
|
5873
|
+
if (cached !== undefined)
|
|
5874
|
+
return cached;
|
|
5875
|
+
for (const key of Object.keys(models)) {
|
|
5876
|
+
if (!meta.modelByName.has(key)) {
|
|
5877
|
+
throw refusal({
|
|
5878
|
+
model: key,
|
|
5879
|
+
path: "models",
|
|
5880
|
+
reason: "unknown_model",
|
|
5881
|
+
message: `$scoped: models map names unknown model "${key}"`
|
|
5882
|
+
});
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
const resolved = new Map;
|
|
5886
|
+
const missing = [];
|
|
5887
|
+
for (const model of meta.models) {
|
|
5888
|
+
const entry = models[model.name] ?? models[model.clientName];
|
|
5889
|
+
if (entry === undefined) {
|
|
5890
|
+
missing.push(model.name);
|
|
5891
|
+
continue;
|
|
5892
|
+
}
|
|
5893
|
+
if (entry === "none") {
|
|
5894
|
+
resolved.set(model.name, { kind: "none" });
|
|
5895
|
+
continue;
|
|
5896
|
+
}
|
|
5897
|
+
if (!isPlainObject5(entry)) {
|
|
5898
|
+
throw refusal({
|
|
5899
|
+
model: model.name,
|
|
5900
|
+
path: "models",
|
|
5901
|
+
reason: "invalid_entry",
|
|
5902
|
+
message: `$scoped: models.${model.name} must be "none", { column } or { through }`
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
const keys = Object.keys(entry).filter((key) => entry[key] !== undefined);
|
|
5906
|
+
const extras = keys.filter((key) => key !== "column" && key !== "through" && key !== "nested");
|
|
5907
|
+
if (extras.length > 0 || "column" in entry === "through" in entry) {
|
|
5908
|
+
throw refusal({
|
|
5909
|
+
model: model.name,
|
|
5910
|
+
path: "models",
|
|
5911
|
+
reason: "invalid_entry",
|
|
5912
|
+
message: `$scoped: models.${model.name} must carry exactly one of column/through (plus optional nested)${extras.length > 0 ? ` \u2014 unknown propert${extras.length === 1 ? "y" : "ies"}: ${extras.join(", ")}` : ""}`
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
const nested = nestedModeOf({ entry, model: model.name });
|
|
5916
|
+
if ("column" in entry) {
|
|
5917
|
+
const field = model.fieldByName.get(String(entry.column));
|
|
5918
|
+
if (field === undefined) {
|
|
5919
|
+
throw refusal({
|
|
5920
|
+
model: model.name,
|
|
5921
|
+
path: "models",
|
|
5922
|
+
reason: "unknown_column",
|
|
5923
|
+
message: `$scoped: models.${model.name}.column names unknown field "${String(entry.column)}"`
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
if (field.isList || field.scalarType === null || !SCOPEABLE_COLUMN_TYPES.includes(field.scalarType)) {
|
|
5927
|
+
throw refusal({
|
|
5928
|
+
model: model.name,
|
|
5929
|
+
path: "models",
|
|
5930
|
+
reason: "column_not_scopeable",
|
|
5931
|
+
message: `$scoped: models.${model.name}.column "${field.name}" must be a non-list String, Int or BigInt field`
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5934
|
+
resolved.set(model.name, { kind: "column", field, nested });
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5937
|
+
const relation = model.relationByName.get(String(entry.through));
|
|
5938
|
+
if (relation === undefined || relation.isList) {
|
|
5939
|
+
throw refusal({
|
|
5940
|
+
model: model.name,
|
|
5941
|
+
path: "models",
|
|
5942
|
+
reason: "unknown_relation",
|
|
5943
|
+
message: `$scoped: models.${model.name}.through must name a to-one relation \u2014 "${String(entry.through)}" ${relation === undefined ? "does not exist" : "is a list relation"}`
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
resolveRelationLink({ meta, model, relation });
|
|
5947
|
+
resolved.set(model.name, { kind: "through", relation, targetModel: relation.targetModel, nested });
|
|
5948
|
+
}
|
|
5949
|
+
if (missing.length > 0) {
|
|
5950
|
+
throw refusal({
|
|
5951
|
+
model: missing[0],
|
|
5952
|
+
path: "models",
|
|
5953
|
+
reason: "unclassified_model",
|
|
5954
|
+
message: `$scoped: the models map must classify EVERY model \u2014 missing: ${missing.join(", ")} (use "none" for global models)`
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
for (const [name, entry] of resolved) {
|
|
5958
|
+
if (entry.kind !== "through")
|
|
5959
|
+
continue;
|
|
5960
|
+
const seen = new Set([name]);
|
|
5961
|
+
let current = entry;
|
|
5962
|
+
while (current.kind === "through") {
|
|
5963
|
+
const nextName = current.targetModel;
|
|
5964
|
+
if (seen.has(nextName)) {
|
|
5965
|
+
throw refusal({
|
|
5966
|
+
model: name,
|
|
5967
|
+
path: "models",
|
|
5968
|
+
reason: "through_chain_invalid",
|
|
5969
|
+
message: `$scoped: models.${name} through-chain cycles (${[...seen, nextName].join(" \u2192 ")})`
|
|
5970
|
+
});
|
|
5971
|
+
}
|
|
5972
|
+
seen.add(nextName);
|
|
5973
|
+
const next = resolved.get(nextName);
|
|
5974
|
+
if (next === undefined || next.kind === "none") {
|
|
5975
|
+
throw refusal({
|
|
5976
|
+
model: name,
|
|
5977
|
+
path: "models",
|
|
5978
|
+
reason: "through_chain_invalid",
|
|
5979
|
+
message: `$scoped: models.${name} through-chain must end on a { column } model \u2014 "${nextName}" is ${next === undefined ? "unclassified" : '"none"'}`
|
|
5980
|
+
});
|
|
5981
|
+
}
|
|
5982
|
+
current = next;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
byMap.set(meta, resolved);
|
|
5986
|
+
resolvedCache.set(models, byMap);
|
|
5987
|
+
return resolved;
|
|
5988
|
+
}
|
|
5989
|
+
function assertScopeType(params) {
|
|
5990
|
+
const { resolved, scope } = params;
|
|
5991
|
+
for (const [name, entry] of resolved) {
|
|
5992
|
+
if (entry.kind !== "column")
|
|
5993
|
+
continue;
|
|
5994
|
+
const scalar = entry.field.scalarType;
|
|
5995
|
+
const ok = scalar === "String" ? typeof scope === "string" : scalar === "Int" ? typeof scope === "number" : typeof scope === "bigint" || typeof scope === "number";
|
|
5996
|
+
if (!ok) {
|
|
5997
|
+
throw refusal({
|
|
5998
|
+
model: name,
|
|
5999
|
+
path: "scope",
|
|
6000
|
+
reason: "scope_type_mismatch",
|
|
6001
|
+
message: `$scoped: scope value (${typeof scope}) does not match ${name}.${entry.field.name} (${scalar})`
|
|
6002
|
+
});
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
function buildPreds(params) {
|
|
6007
|
+
const { resolved, scope } = params;
|
|
6008
|
+
const preds = new Map;
|
|
6009
|
+
const predOf = (name) => {
|
|
6010
|
+
const known = preds.get(name);
|
|
6011
|
+
if (known !== undefined)
|
|
6012
|
+
return known;
|
|
6013
|
+
const entry = resolved.get(name);
|
|
6014
|
+
const value = entry.kind === "none" ? null : entry.kind === "column" ? { [entry.field.name]: scope } : { [entry.relation.name]: predOf(entry.targetModel) };
|
|
6015
|
+
preds.set(name, value);
|
|
6016
|
+
return value;
|
|
6017
|
+
};
|
|
6018
|
+
for (const name of resolved.keys())
|
|
6019
|
+
predOf(name);
|
|
6020
|
+
return preds;
|
|
6021
|
+
}
|
|
6022
|
+
function entryOf(ctx, model) {
|
|
6023
|
+
return ctx.resolved.get(model.name);
|
|
6024
|
+
}
|
|
6025
|
+
function requirePred(params) {
|
|
6026
|
+
const { ctx, model, path } = params;
|
|
6027
|
+
if (ctx.scope === null) {
|
|
6028
|
+
throw refusal({
|
|
6029
|
+
model: model.name,
|
|
6030
|
+
path,
|
|
6031
|
+
reason: "null_scope",
|
|
6032
|
+
message: `$scoped: scope is null \u2014 calls touching scoped model ${model.name} refuse until a scope is bound`
|
|
6033
|
+
});
|
|
6034
|
+
}
|
|
6035
|
+
return ctx.preds.get(model.name);
|
|
6036
|
+
}
|
|
6037
|
+
function mergeWhere2(params) {
|
|
6038
|
+
const { where, pred } = params;
|
|
6039
|
+
if (where === undefined)
|
|
6040
|
+
return pred;
|
|
6041
|
+
return { AND: [where, pred] };
|
|
6042
|
+
}
|
|
6043
|
+
function injectUniqueWhere(params) {
|
|
6044
|
+
const { where, pred } = params;
|
|
6045
|
+
if (!isPlainObject5(where))
|
|
6046
|
+
return { AND: [pred] };
|
|
6047
|
+
const existing = where.AND === undefined ? [] : Array.isArray(where.AND) ? where.AND : [where.AND];
|
|
6048
|
+
return { ...where, AND: [...existing, pred] };
|
|
6049
|
+
}
|
|
6050
|
+
function flattenUniqueWhere(params) {
|
|
6051
|
+
const { model, where } = params;
|
|
6052
|
+
if (model.compoundUniques.size === 0)
|
|
6053
|
+
return where;
|
|
6054
|
+
const out = {};
|
|
6055
|
+
for (const [key, value] of Object.entries(where)) {
|
|
6056
|
+
if (isPlainObject5(value) && model.compoundUniques.has(key))
|
|
6057
|
+
Object.assign(out, value);
|
|
6058
|
+
else
|
|
6059
|
+
out[key] = value;
|
|
6060
|
+
}
|
|
6061
|
+
return out;
|
|
6062
|
+
}
|
|
6063
|
+
function isTenancyInverse(params) {
|
|
6064
|
+
const { ctx, model, relation } = params;
|
|
6065
|
+
const targetEntry = ctx.resolved.get(relation.targetModel);
|
|
6066
|
+
if (targetEntry === undefined || targetEntry.kind !== "through")
|
|
6067
|
+
return false;
|
|
6068
|
+
const through = targetEntry.relation;
|
|
6069
|
+
return through.targetModel === model.name && (through.relationName ?? null) === (relation.relationName ?? null) && !(relation.model === relation.targetModel && through.name === relation.name);
|
|
6070
|
+
}
|
|
6071
|
+
function walkWhere(params) {
|
|
6072
|
+
const { ctx, model, where, path } = params;
|
|
6073
|
+
const out = {};
|
|
6074
|
+
const extra = [];
|
|
6075
|
+
const walkGroup = (value, key) => {
|
|
6076
|
+
const walkItem = (item) => isPlainObject5(item) ? walkWhere({ ctx, model, where: item, path: `${path}.${key}` }) : item;
|
|
6077
|
+
return Array.isArray(value) ? value.map(walkItem) : walkItem(value);
|
|
6078
|
+
};
|
|
6079
|
+
for (const [key, value] of Object.entries(where)) {
|
|
6080
|
+
if (value === undefined)
|
|
6081
|
+
continue;
|
|
6082
|
+
if (key === "AND" || key === "OR" || key === "NOT") {
|
|
6083
|
+
out[key] = walkGroup(value, key);
|
|
6084
|
+
continue;
|
|
6085
|
+
}
|
|
6086
|
+
const relation = model.relationByName.get(key);
|
|
6087
|
+
if (relation === undefined) {
|
|
6088
|
+
out[key] = value;
|
|
6089
|
+
continue;
|
|
6090
|
+
}
|
|
6091
|
+
const target = ctx.meta.modelByName.get(relation.targetModel);
|
|
6092
|
+
const targetEntry = entryOf(ctx, target);
|
|
6093
|
+
const scoped = targetEntry.kind !== "none";
|
|
6094
|
+
const childPath = `${path}.${key}`;
|
|
6095
|
+
const pred = () => requirePred({ ctx, model: target, path: childPath });
|
|
6096
|
+
const walkChild = (child, op) => isPlainObject5(child) ? walkWhere({ ctx, model: target, where: child, path: `${childPath}.${op}` }) : child;
|
|
6097
|
+
const parts = [];
|
|
6098
|
+
if (relation.isList) {
|
|
6099
|
+
if (!isPlainObject5(value)) {
|
|
6100
|
+
out[key] = value;
|
|
6101
|
+
continue;
|
|
6102
|
+
}
|
|
6103
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
6104
|
+
if (operand === undefined)
|
|
6105
|
+
continue;
|
|
6106
|
+
if (op === "some" || op === "none") {
|
|
6107
|
+
const walked = walkChild(operand, op);
|
|
6108
|
+
parts.push({ [op]: scoped ? { AND: [walked, pred()] } : walked });
|
|
6109
|
+
} else if (op === "every") {
|
|
6110
|
+
const walked = walkChild(operand, op);
|
|
6111
|
+
parts.push(scoped ? { none: { AND: [pred(), { NOT: [walked] }] } } : { every: walked });
|
|
6112
|
+
} else {
|
|
6113
|
+
parts.push({ [op]: operand });
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
} else {
|
|
6117
|
+
const arms = [];
|
|
6118
|
+
if (value === null) {
|
|
6119
|
+
arms.push({ arm: "is", operand: null });
|
|
6120
|
+
} else if (isPlainObject5(value) && (("is" in value) || ("isNot" in value))) {
|
|
6121
|
+
let mixed = false;
|
|
6122
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
6123
|
+
if (operand === undefined)
|
|
6124
|
+
continue;
|
|
6125
|
+
if (op === "is" || op === "isNot")
|
|
6126
|
+
arms.push({ arm: op, operand });
|
|
6127
|
+
else
|
|
6128
|
+
mixed = true;
|
|
6129
|
+
}
|
|
6130
|
+
if (mixed) {
|
|
6131
|
+
out[key] = value;
|
|
6132
|
+
continue;
|
|
6133
|
+
}
|
|
6134
|
+
} else if (isPlainObject5(value)) {
|
|
6135
|
+
arms.push({ arm: "is", operand: value });
|
|
6136
|
+
} else {
|
|
6137
|
+
out[key] = value;
|
|
6138
|
+
continue;
|
|
6139
|
+
}
|
|
6140
|
+
for (const { arm, operand } of arms) {
|
|
6141
|
+
if (operand === null) {
|
|
6142
|
+
parts.push(scoped ? arm === "is" ? { isNot: pred() } : { is: pred() } : { [arm]: null });
|
|
6143
|
+
} else {
|
|
6144
|
+
const walked = walkChild(operand, arm);
|
|
6145
|
+
parts.push({ [arm]: scoped ? { AND: [walked, pred()] } : walked });
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
if (parts.length === 0) {
|
|
6150
|
+
out[key] = value;
|
|
6151
|
+
continue;
|
|
6152
|
+
}
|
|
6153
|
+
const combined = {};
|
|
6154
|
+
for (const part of parts) {
|
|
6155
|
+
const opKey = Object.keys(part)[0];
|
|
6156
|
+
if (opKey in combined)
|
|
6157
|
+
extra.push({ [key]: part });
|
|
6158
|
+
else
|
|
6159
|
+
combined[opKey] = part[opKey];
|
|
6160
|
+
}
|
|
6161
|
+
out[key] = combined;
|
|
6162
|
+
}
|
|
6163
|
+
if (extra.length > 0) {
|
|
6164
|
+
const existing = out.AND === undefined ? [] : Array.isArray(out.AND) ? out.AND : [out.AND];
|
|
6165
|
+
out.AND = [...existing, ...extra];
|
|
6166
|
+
}
|
|
6167
|
+
return out;
|
|
6168
|
+
}
|
|
6169
|
+
function walkOrderTerm(params) {
|
|
6170
|
+
const { ctx, model, term, path } = params;
|
|
6171
|
+
const entry = entryOf(ctx, model);
|
|
6172
|
+
for (const [key, spec] of Object.entries(term)) {
|
|
6173
|
+
if (spec === undefined)
|
|
6174
|
+
continue;
|
|
6175
|
+
const relation = model.relationByName.get(key);
|
|
6176
|
+
if (relation === undefined)
|
|
6177
|
+
continue;
|
|
6178
|
+
const target = ctx.meta.modelByName.get(relation.targetModel);
|
|
6179
|
+
const targetEntry = entryOf(ctx, target);
|
|
6180
|
+
const tenancyEdge = entry.kind === "through" && entry.relation.name === key;
|
|
6181
|
+
if (targetEntry.kind !== "none" && !tenancyEdge) {
|
|
6182
|
+
throw refusal({
|
|
6183
|
+
model: target.name,
|
|
6184
|
+
path: `${path}.${key}`,
|
|
6185
|
+
reason: "relation_orderby_on_scoped",
|
|
6186
|
+
message: `$scoped: orderBy through ${model.name}.${key} reaches scoped model ${target.name} \u2014 ordering by a foreign row's values would leak; order by own columns instead`
|
|
6187
|
+
});
|
|
6188
|
+
}
|
|
6189
|
+
if (isPlainObject5(spec)) {
|
|
6190
|
+
walkOrderTerm({ ctx, model: target, term: spec, path: `${path}.${key}` });
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
function walkOrderBy(params) {
|
|
6195
|
+
const { ctx, model, orderBy, path } = params;
|
|
6196
|
+
const terms = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
6197
|
+
for (const term of terms) {
|
|
6198
|
+
if (isPlainObject5(term))
|
|
6199
|
+
walkOrderTerm({ ctx, model, term, path });
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
function walkCountSpec(params) {
|
|
6203
|
+
const { ctx, model, spec, path } = params;
|
|
6204
|
+
const guard = (relation) => {
|
|
6205
|
+
const target = ctx.meta.modelByName.get(relation.targetModel);
|
|
6206
|
+
const targetEntry = entryOf(ctx, target);
|
|
6207
|
+
if (targetEntry.kind === "none")
|
|
6208
|
+
return;
|
|
6209
|
+
if (isTenancyInverse({ ctx, model, relation }))
|
|
6210
|
+
return;
|
|
6211
|
+
throw refusal({
|
|
6212
|
+
model: target.name,
|
|
6213
|
+
path: `${path}.${relation.name}`,
|
|
6214
|
+
reason: "count_on_scoped_relation",
|
|
6215
|
+
message: `$scoped: _count of ${model.name}.${relation.name} would count ${target.name} rows across tenants (relation counts take no filter) \u2014 select the relation with a where instead`
|
|
6216
|
+
});
|
|
6217
|
+
};
|
|
6218
|
+
if (spec === true) {
|
|
6219
|
+
for (const relation of model.relations) {
|
|
6220
|
+
if (relation.isList)
|
|
6221
|
+
guard(relation);
|
|
6222
|
+
}
|
|
6223
|
+
return;
|
|
6224
|
+
}
|
|
6225
|
+
if (!isPlainObject5(spec) || !isPlainObject5(spec.select))
|
|
6226
|
+
return;
|
|
6227
|
+
for (const [name, flag] of Object.entries(spec.select)) {
|
|
6228
|
+
if (flag === undefined || flag === false)
|
|
6229
|
+
continue;
|
|
6230
|
+
const relation = model.relationByName.get(name);
|
|
6231
|
+
if (relation !== undefined)
|
|
6232
|
+
guard(relation);
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
function walkSelection(params) {
|
|
6236
|
+
const { ctx, model, args, path } = params;
|
|
6237
|
+
for (const blockKey of ["include", "select"]) {
|
|
6238
|
+
const block = args[blockKey];
|
|
6239
|
+
if (!isPlainObject5(block))
|
|
6240
|
+
continue;
|
|
6241
|
+
const next = { ...block };
|
|
6242
|
+
for (const [name, spec] of Object.entries(block)) {
|
|
6243
|
+
if (spec === undefined || spec === false)
|
|
6244
|
+
continue;
|
|
6245
|
+
const blockPath = `${path}.${blockKey}`;
|
|
6246
|
+
if (name === "_count") {
|
|
6247
|
+
walkCountSpec({ ctx, model, spec, path: `${blockPath}._count` });
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
const relation = model.relationByName.get(name);
|
|
6251
|
+
if (relation === undefined)
|
|
6252
|
+
continue;
|
|
6253
|
+
const target = ctx.meta.modelByName.get(relation.targetModel);
|
|
6254
|
+
const targetEntry = entryOf(ctx, target);
|
|
6255
|
+
const scoped = targetEntry.kind !== "none";
|
|
6256
|
+
const childPath = `${blockPath}.${name}`;
|
|
6257
|
+
if (spec === true && !scoped)
|
|
6258
|
+
continue;
|
|
6259
|
+
const child = spec === true ? {} : isPlainObject5(spec) ? { ...spec } : spec;
|
|
6260
|
+
if (!isPlainObject5(child))
|
|
6261
|
+
continue;
|
|
6262
|
+
const walkedWhere = isPlainObject5(child.where) ? walkWhere({ ctx, model: target, where: child.where, path: `${childPath}.where` }) : child.where;
|
|
6263
|
+
if (scoped) {
|
|
6264
|
+
child.where = mergeWhere2({ where: walkedWhere, pred: requirePred({ ctx, model: target, path: childPath }) });
|
|
6265
|
+
} else if (walkedWhere !== undefined) {
|
|
6266
|
+
child.where = walkedWhere;
|
|
6267
|
+
}
|
|
6268
|
+
if (child.orderBy !== undefined) {
|
|
6269
|
+
walkOrderBy({ ctx, model: target, orderBy: child.orderBy, path: `${childPath}.orderBy` });
|
|
6270
|
+
}
|
|
6271
|
+
walkSelection({ ctx, model: target, args: child, path: childPath });
|
|
6272
|
+
next[name] = child;
|
|
6273
|
+
}
|
|
6274
|
+
args[blockKey] = next;
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
var REFERENCE_VERBS2 = [
|
|
6278
|
+
"connect",
|
|
6279
|
+
"connectOrCreate",
|
|
6280
|
+
"set",
|
|
6281
|
+
"disconnect",
|
|
6282
|
+
"update",
|
|
6283
|
+
"updateMany",
|
|
6284
|
+
"upsert",
|
|
6285
|
+
"delete",
|
|
6286
|
+
"deleteMany"
|
|
6287
|
+
];
|
|
6288
|
+
function nestedModeFor(entry) {
|
|
6289
|
+
return entry.kind === "none" ? "verify" : entry.nested;
|
|
6290
|
+
}
|
|
6291
|
+
function mapItems(value, fn) {
|
|
6292
|
+
return Array.isArray(value) ? value.map(fn) : fn(value);
|
|
6293
|
+
}
|
|
6294
|
+
function walkCreateData(params) {
|
|
6295
|
+
const { ctx, model, data, path } = params;
|
|
6296
|
+
if (!isPlainObject5(data))
|
|
6297
|
+
return data;
|
|
6298
|
+
const entry = entryOf(ctx, model);
|
|
6299
|
+
const out = { ...data };
|
|
6300
|
+
if (entry.kind === "column") {
|
|
6301
|
+
requirePred({ ctx, model, path });
|
|
6302
|
+
const given = out[entry.field.name];
|
|
6303
|
+
if (given !== undefined && given !== ctx.scope) {
|
|
6304
|
+
throw refusal({
|
|
6305
|
+
model: model.name,
|
|
6306
|
+
path: `${path}.${entry.field.name}`,
|
|
6307
|
+
reason: "explicit_scope_mismatch",
|
|
6308
|
+
message: `$scoped: create on ${model.name} sets ${entry.field.name} to ${given === null ? "null" : "a different tenant"} \u2014 the scoped handle owns that column`
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
out[entry.field.name] = ctx.scope;
|
|
6312
|
+
}
|
|
6313
|
+
if (entry.kind === "through" && params.viaTenancyEdge !== true) {
|
|
6314
|
+
requirePred({ ctx, model, path });
|
|
6315
|
+
const link = resolveRelationLink({ meta: ctx.meta, model, relation: entry.relation });
|
|
6316
|
+
if (link.kind === "toOneOwner" && out[link.parentKey.name] !== undefined) {
|
|
6317
|
+
throw refusal({
|
|
6318
|
+
model: model.name,
|
|
6319
|
+
path: `${path}.${link.parentKey.name}`,
|
|
6320
|
+
reason: "through_fk_write",
|
|
6321
|
+
message: `$scoped: writing ${model.name}.${link.parentKey.name} directly would bypass tenant verification \u2014 use ${entry.relation.name}: { connect: \u2026 } instead`
|
|
6322
|
+
});
|
|
6323
|
+
}
|
|
6324
|
+
if (out[entry.relation.name] === undefined) {
|
|
6325
|
+
throw refusal({
|
|
6326
|
+
model: model.name,
|
|
6327
|
+
path,
|
|
6328
|
+
reason: "through_fk_write",
|
|
6329
|
+
message: `$scoped: create on ${model.name} must bind its tenancy parent \u2014 provide ${entry.relation.name}: { connect | create | connectOrCreate: \u2026 }`
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
for (const [key, value] of Object.entries(out)) {
|
|
6334
|
+
const relation = model.relationByName.get(key);
|
|
6335
|
+
if (relation === undefined || !isPlainObject5(value))
|
|
6336
|
+
continue;
|
|
6337
|
+
out[key] = walkNestedVerbs({ ctx, model, relation, verbs: value, path: `${path}.${key}`, phase: "create" });
|
|
6338
|
+
}
|
|
6339
|
+
return out;
|
|
6340
|
+
}
|
|
6341
|
+
function walkUpdateData(params) {
|
|
6342
|
+
const { ctx, model, data, path } = params;
|
|
6343
|
+
if (!isPlainObject5(data))
|
|
6344
|
+
return data;
|
|
6345
|
+
const entry = entryOf(ctx, model);
|
|
6346
|
+
const out = { ...data };
|
|
6347
|
+
if (entry.kind === "column") {
|
|
6348
|
+
const given = out[entry.field.name];
|
|
6349
|
+
if (given !== undefined) {
|
|
6350
|
+
const setValue = isPlainObject5(given) && Object.keys(given).length === 1 ? given.set : given;
|
|
6351
|
+
if (setValue !== ctx.scope) {
|
|
6352
|
+
throw refusal({
|
|
6353
|
+
model: model.name,
|
|
6354
|
+
path: `${path}.${entry.field.name}`,
|
|
6355
|
+
reason: "explicit_scope_mismatch",
|
|
6356
|
+
message: `$scoped: update on ${model.name} writes ${entry.field.name} \u2014 a scoped handle cannot move rows between tenants`
|
|
6357
|
+
});
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
if (entry.kind === "through") {
|
|
6362
|
+
const link = resolveRelationLink({ meta: ctx.meta, model, relation: entry.relation });
|
|
6363
|
+
if (link.kind === "toOneOwner" && out[link.parentKey.name] !== undefined) {
|
|
6364
|
+
throw refusal({
|
|
6365
|
+
model: model.name,
|
|
6366
|
+
path: `${path}.${link.parentKey.name}`,
|
|
6367
|
+
reason: "through_fk_write",
|
|
6368
|
+
message: `$scoped: writing ${model.name}.${link.parentKey.name} directly would bypass tenant verification \u2014 use ${entry.relation.name}: { connect: \u2026 } instead`
|
|
6369
|
+
});
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
for (const [key, value] of Object.entries(out)) {
|
|
6373
|
+
const relation = model.relationByName.get(key);
|
|
6374
|
+
if (relation === undefined || !isPlainObject5(value))
|
|
6375
|
+
continue;
|
|
6376
|
+
out[key] = walkNestedVerbs({ ctx, model, relation, verbs: value, path: `${path}.${key}`, phase: "update" });
|
|
6377
|
+
}
|
|
6378
|
+
return out;
|
|
6379
|
+
}
|
|
6380
|
+
function walkNestedVerbs(params) {
|
|
6381
|
+
const { ctx, model, relation, verbs, path, phase } = params;
|
|
6382
|
+
const target = ctx.meta.modelByName.get(relation.targetModel);
|
|
6383
|
+
const targetEntry = entryOf(ctx, target);
|
|
6384
|
+
const scoped = targetEntry.kind !== "none";
|
|
6385
|
+
const modelEntry = entryOf(ctx, model);
|
|
6386
|
+
const pairedDown = scoped && isTenancyInverse({ ctx, model, relation });
|
|
6387
|
+
const tenancyEdge = modelEntry.kind === "through" && modelEntry.relation.name === relation.name;
|
|
6388
|
+
const implicitSafe = pairedDown || tenancyEdge;
|
|
6389
|
+
const mode = nestedModeFor(targetEntry);
|
|
6390
|
+
const out = { ...verbs };
|
|
6391
|
+
const pred = (verbPath) => requirePred({ ctx, model: target, path: verbPath });
|
|
6392
|
+
const refuse = (verb, why) => {
|
|
6393
|
+
throw refusal({
|
|
6394
|
+
model: target.name,
|
|
6395
|
+
path: `${path}.${verb}`,
|
|
6396
|
+
reason: "nested_refused",
|
|
6397
|
+
message: `$scoped: nested "${verb}" on ${model.name}.${relation.name} (${target.name}) ${why}`
|
|
6398
|
+
});
|
|
6399
|
+
};
|
|
6400
|
+
const walkTargetWhere = (where, verbPath) => isPlainObject5(where) ? walkWhere({ ctx, model: target, where, path: verbPath }) : where;
|
|
6401
|
+
const uniqueWhere = (where, verbPath) => {
|
|
6402
|
+
const walked = walkTargetWhere(where, verbPath);
|
|
6403
|
+
return scoped ? injectUniqueWhere({ where: walked, pred: pred(verbPath) }) : walked;
|
|
6404
|
+
};
|
|
6405
|
+
const filterWhere = (where, verbPath) => {
|
|
6406
|
+
const walked = walkTargetWhere(where, verbPath);
|
|
6407
|
+
return scoped ? mergeWhere2({ where: walked ?? {}, pred: pred(verbPath) }) : walked;
|
|
6408
|
+
};
|
|
6409
|
+
const walkChildCreate = (item, verbPath) => walkCreateData({ ctx, model: target, data: item, path: verbPath, viaTenancyEdge: pairedDown });
|
|
6410
|
+
const walkChildUpdate = (item, verbPath) => walkUpdateData({ ctx, model: target, data: item, path: verbPath });
|
|
6411
|
+
const link = () => resolveRelationLink({ meta: ctx.meta, model, relation });
|
|
6412
|
+
for (const [verb, operand] of Object.entries(verbs)) {
|
|
6413
|
+
if (operand === undefined)
|
|
6414
|
+
continue;
|
|
6415
|
+
const verbPath = `${path}.${verb}`;
|
|
6416
|
+
if (verb === "create") {
|
|
6417
|
+
out[verb] = mapItems(operand, (item) => walkChildCreate(item, verbPath));
|
|
6418
|
+
continue;
|
|
6419
|
+
}
|
|
6420
|
+
if (scoped && mode === "refuse" && REFERENCE_VERBS2.includes(verb)) {
|
|
6421
|
+
refuse(verb, `is disabled \u2014 ${target.name} is configured nested: "refuse"; go through the base client or its own delegate`);
|
|
6422
|
+
}
|
|
6423
|
+
switch (verb) {
|
|
6424
|
+
case "connect":
|
|
6425
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? uniqueWhere(item, verbPath) : item);
|
|
6426
|
+
break;
|
|
6427
|
+
case "connectOrCreate":
|
|
6428
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? { ...item, where: uniqueWhere(item.where, verbPath), create: walkChildCreate(item.create, verbPath) } : item);
|
|
6429
|
+
break;
|
|
6430
|
+
case "set": {
|
|
6431
|
+
if (phase === "create")
|
|
6432
|
+
break;
|
|
6433
|
+
if (scoped) {
|
|
6434
|
+
const kind = link().kind;
|
|
6435
|
+
if (kind === "manyToMany")
|
|
6436
|
+
refuse(verb, "clears the whole join set \u2014 unlinking other tenants' rows cannot be verified; disconnect explicit rows instead");
|
|
6437
|
+
if (!implicitSafe)
|
|
6438
|
+
refuse(verb, "first detaches EVERY current child, which cannot be tenant-filtered on this edge; disconnect explicit rows instead");
|
|
6439
|
+
}
|
|
6440
|
+
out[verb] = mapItems(operand ?? [], (item) => isPlainObject5(item) ? uniqueWhere(item, verbPath) : item);
|
|
6441
|
+
break;
|
|
6442
|
+
}
|
|
6443
|
+
case "disconnect": {
|
|
6444
|
+
if (!relation.isList) {
|
|
6445
|
+
if (scoped) {
|
|
6446
|
+
const kind = link().kind;
|
|
6447
|
+
if (kind === "toOneOwner") {
|
|
6448
|
+
if (tenancyEdge) {
|
|
6449
|
+
refuse(verb, "would orphan the row out of its tenant (through-parent set to null) \u2014 delete it or connect a replacement");
|
|
6450
|
+
}
|
|
6451
|
+
} else if (!implicitSafe) {
|
|
6452
|
+
refuse(verb, "targets whatever row currently points here \u2014 not tenant-verifiable on this edge");
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
break;
|
|
6456
|
+
}
|
|
6457
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? uniqueWhere(item, verbPath) : item);
|
|
6458
|
+
break;
|
|
6459
|
+
}
|
|
6460
|
+
case "delete": {
|
|
6461
|
+
if (!relation.isList) {
|
|
6462
|
+
if (scoped && !implicitSafe) {
|
|
6463
|
+
refuse(verb, "targets whatever row is currently connected \u2014 not tenant-verifiable on this edge");
|
|
6464
|
+
}
|
|
6465
|
+
break;
|
|
6466
|
+
}
|
|
6467
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? uniqueWhere(item, verbPath) : item);
|
|
6468
|
+
break;
|
|
6469
|
+
}
|
|
6470
|
+
case "update": {
|
|
6471
|
+
if (!relation.isList) {
|
|
6472
|
+
if (scoped && !implicitSafe) {
|
|
6473
|
+
refuse(verb, "targets whatever row is currently connected \u2014 not tenant-verifiable on this edge; update it through its own delegate");
|
|
6474
|
+
}
|
|
6475
|
+
out[verb] = isPlainObject5(operand) ? "data" in operand && isPlainObject5(operand.data) ? { ...operand, data: walkChildUpdate(operand.data, verbPath) } : walkChildUpdate(operand, verbPath) : operand;
|
|
6476
|
+
break;
|
|
6477
|
+
}
|
|
6478
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? { ...item, where: uniqueWhere(item.where, verbPath), data: walkChildUpdate(item.data, verbPath) } : item);
|
|
6479
|
+
break;
|
|
6480
|
+
}
|
|
6481
|
+
case "updateMany":
|
|
6482
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? { ...item, where: filterWhere(item.where, verbPath), data: walkChildUpdate(item.data, verbPath) } : item);
|
|
6483
|
+
break;
|
|
6484
|
+
case "deleteMany":
|
|
6485
|
+
out[verb] = mapItems(operand, (item) => filterWhere(item, verbPath));
|
|
6486
|
+
break;
|
|
6487
|
+
case "upsert": {
|
|
6488
|
+
if (!relation.isList) {
|
|
6489
|
+
if (scoped && !implicitSafe) {
|
|
6490
|
+
refuse(verb, "updates whatever row is currently connected \u2014 not tenant-verifiable on this edge");
|
|
6491
|
+
}
|
|
6492
|
+
out[verb] = isPlainObject5(operand) ? {
|
|
6493
|
+
...operand,
|
|
6494
|
+
create: walkChildCreate(operand.create, verbPath),
|
|
6495
|
+
update: walkChildUpdate(operand.update, verbPath)
|
|
6496
|
+
} : operand;
|
|
6497
|
+
break;
|
|
6498
|
+
}
|
|
6499
|
+
out[verb] = mapItems(operand, (item) => isPlainObject5(item) ? {
|
|
6500
|
+
...item,
|
|
6501
|
+
where: uniqueWhere(item.where, verbPath),
|
|
6502
|
+
create: walkChildCreate(item.create, verbPath),
|
|
6503
|
+
update: walkChildUpdate(item.update, verbPath)
|
|
6504
|
+
} : item);
|
|
6505
|
+
break;
|
|
6506
|
+
}
|
|
6507
|
+
default:
|
|
6508
|
+
break;
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
return out;
|
|
6512
|
+
}
|
|
6513
|
+
function createScopeRewriter(params) {
|
|
6514
|
+
const { meta, models, scope } = params;
|
|
6515
|
+
const resolved = resolveScopedModels({ meta, models });
|
|
6516
|
+
if (scope !== null)
|
|
6517
|
+
assertScopeType({ meta, resolved, scope });
|
|
6518
|
+
const preds = scope === null ? new Map : buildPreds({ meta, resolved, scope });
|
|
6519
|
+
return {
|
|
6520
|
+
rewrite: (call) => rewriteScopedCall({
|
|
6521
|
+
meta,
|
|
6522
|
+
resolved,
|
|
6523
|
+
scope,
|
|
6524
|
+
preds,
|
|
6525
|
+
model: call.model,
|
|
6526
|
+
method: call.method,
|
|
6527
|
+
args: call.args
|
|
6528
|
+
})
|
|
6529
|
+
};
|
|
6530
|
+
}
|
|
6531
|
+
var FIND_LIKE = ["findMany", "findFirst", "findFirstOrThrow", "count", "aggregate", "groupBy"];
|
|
6532
|
+
function rewriteScopedCall(params) {
|
|
6533
|
+
const { meta, resolved, scope, preds, method } = params;
|
|
6534
|
+
const ctx = { meta, resolved, scope, preds };
|
|
6535
|
+
const model = getModelMeta({ meta, model: params.model });
|
|
6536
|
+
const entry = entryOf(ctx, model);
|
|
6537
|
+
const scoped = entry.kind !== "none";
|
|
6538
|
+
const root = `${model.clientName}.${method}`;
|
|
6539
|
+
const args = { ...params.args ?? {} };
|
|
6540
|
+
const ownPred = () => requirePred({ ctx, model, path: root });
|
|
6541
|
+
const walkOwnWhere = (where, path) => isPlainObject5(where) ? walkWhere({ ctx, model, where, path }) : where;
|
|
6542
|
+
if (FIND_LIKE.includes(method)) {
|
|
6543
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6544
|
+
if (scoped)
|
|
6545
|
+
args.where = mergeWhere2({ where: walked, pred: ownPred() });
|
|
6546
|
+
else if (walked !== undefined)
|
|
6547
|
+
args.where = walked;
|
|
6548
|
+
if (args.orderBy !== undefined)
|
|
6549
|
+
walkOrderBy({ ctx, model, orderBy: args.orderBy, path: `${root}.orderBy` });
|
|
6550
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6551
|
+
return { model: model.name, method, args };
|
|
6552
|
+
}
|
|
6553
|
+
switch (method) {
|
|
6554
|
+
case "findUnique":
|
|
6555
|
+
case "findUniqueOrThrow": {
|
|
6556
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6557
|
+
if (!scoped) {
|
|
6558
|
+
const walked2 = walkOwnWhere(args.where, `${root}.where`);
|
|
6559
|
+
if (walked2 !== undefined)
|
|
6560
|
+
args.where = walked2;
|
|
6561
|
+
return { model: model.name, method, args };
|
|
6562
|
+
}
|
|
6563
|
+
const flat = isPlainObject5(args.where) ? flattenUniqueWhere({ model, where: args.where }) : args.where;
|
|
6564
|
+
const walked = isPlainObject5(flat) ? walkWhere({ ctx, model, where: flat, path: `${root}.where` }) : flat;
|
|
6565
|
+
args.where = mergeWhere2({ where: walked, pred: ownPred() });
|
|
6566
|
+
return {
|
|
6567
|
+
model: model.name,
|
|
6568
|
+
method: method === "findUnique" ? "findFirst" : "findFirstOrThrow",
|
|
6569
|
+
args
|
|
6570
|
+
};
|
|
6571
|
+
}
|
|
6572
|
+
case "create": {
|
|
6573
|
+
args.data = walkCreateData({ ctx, model, data: args.data ?? {}, path: `${root}.data` });
|
|
6574
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6575
|
+
return { model: model.name, method, args };
|
|
6576
|
+
}
|
|
6577
|
+
case "createMany":
|
|
6578
|
+
case "createManyAndReturn": {
|
|
6579
|
+
const rows = args.data;
|
|
6580
|
+
args.data = Array.isArray(rows) ? rows.map((row) => walkCreateData({ ctx, model, data: row, path: `${root}.data` })) : walkCreateData({ ctx, model, data: rows ?? {}, path: `${root}.data` });
|
|
6581
|
+
if (method === "createManyAndReturn")
|
|
6582
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6583
|
+
return { model: model.name, method, args };
|
|
6584
|
+
}
|
|
6585
|
+
case "update": {
|
|
6586
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6587
|
+
args.where = scoped ? injectUniqueWhere({ where: walked, pred: ownPred() }) : walked;
|
|
6588
|
+
args.data = walkUpdateData({ ctx, model, data: args.data ?? {}, path: `${root}.data` });
|
|
6589
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6590
|
+
return { model: model.name, method, args };
|
|
6591
|
+
}
|
|
6592
|
+
case "updateMany": {
|
|
6593
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6594
|
+
if (scoped)
|
|
6595
|
+
args.where = mergeWhere2({ where: walked, pred: ownPred() });
|
|
6596
|
+
else if (walked !== undefined)
|
|
6597
|
+
args.where = walked;
|
|
6598
|
+
args.data = walkUpdateData({ ctx, model, data: args.data ?? {}, path: `${root}.data` });
|
|
6599
|
+
return { model: model.name, method, args };
|
|
6600
|
+
}
|
|
6601
|
+
case "upsert": {
|
|
6602
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6603
|
+
args.where = scoped ? injectUniqueWhere({ where: walked, pred: ownPred() }) : walked;
|
|
6604
|
+
args.create = walkCreateData({ ctx, model, data: args.create ?? {}, path: `${root}.create` });
|
|
6605
|
+
args.update = walkUpdateData({ ctx, model, data: args.update ?? {}, path: `${root}.update` });
|
|
6606
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6607
|
+
return { model: model.name, method, args };
|
|
6608
|
+
}
|
|
6609
|
+
case "delete": {
|
|
6610
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6611
|
+
args.where = scoped ? injectUniqueWhere({ where: walked, pred: ownPred() }) : walked;
|
|
6612
|
+
walkSelection({ ctx, model, args, path: root });
|
|
6613
|
+
return { model: model.name, method, args };
|
|
6614
|
+
}
|
|
6615
|
+
case "deleteMany": {
|
|
6616
|
+
const walked = walkOwnWhere(args.where, `${root}.where`);
|
|
6617
|
+
if (scoped)
|
|
6618
|
+
args.where = mergeWhere2({ where: walked, pred: ownPred() });
|
|
6619
|
+
else if (walked !== undefined)
|
|
6620
|
+
args.where = walked;
|
|
6621
|
+
return { model: model.name, method, args };
|
|
6622
|
+
}
|
|
6623
|
+
default:
|
|
6624
|
+
return { model: model.name, method, args: params.args };
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
function scopedDelegate2(params) {
|
|
6628
|
+
const { ctx, model, base } = params;
|
|
6629
|
+
const run = (method, args) => {
|
|
6630
|
+
let rewritten;
|
|
6631
|
+
try {
|
|
6632
|
+
rewritten = rewriteScopedCall({
|
|
6633
|
+
meta: ctx.meta,
|
|
6634
|
+
resolved: ctx.resolved,
|
|
6635
|
+
scope: ctx.scope,
|
|
6636
|
+
preds: ctx.preds,
|
|
6637
|
+
model: model.name,
|
|
6638
|
+
method,
|
|
6639
|
+
args
|
|
6640
|
+
});
|
|
6641
|
+
} catch (error) {
|
|
6642
|
+
return Promise.reject(error);
|
|
6643
|
+
}
|
|
6644
|
+
const target = base[rewritten.method];
|
|
6645
|
+
return target(rewritten.args);
|
|
6646
|
+
};
|
|
6647
|
+
return {
|
|
6648
|
+
findMany: (args) => run("findMany", args),
|
|
6649
|
+
findFirst: (args) => run("findFirst", args),
|
|
6650
|
+
findFirstOrThrow: (args) => run("findFirstOrThrow", args),
|
|
6651
|
+
findUnique: (args) => run("findUnique", args),
|
|
6652
|
+
findUniqueOrThrow: (args) => run("findUniqueOrThrow", args),
|
|
6653
|
+
create: (args) => run("create", args),
|
|
6654
|
+
createMany: (args) => run("createMany", args),
|
|
6655
|
+
createManyAndReturn: (args) => run("createManyAndReturn", args),
|
|
6656
|
+
update: (args) => run("update", args),
|
|
6657
|
+
updateMany: (args) => run("updateMany", args),
|
|
6658
|
+
upsert: (args) => run("upsert", args),
|
|
6659
|
+
delete: (args) => run("delete", args),
|
|
6660
|
+
deleteMany: (args) => run("deleteMany", args),
|
|
6661
|
+
count: (args) => run("count", args),
|
|
6662
|
+
aggregate: (args) => run("aggregate", args),
|
|
6663
|
+
groupBy: (args) => run("groupBy", args)
|
|
6664
|
+
};
|
|
6665
|
+
}
|
|
6666
|
+
function scopeExtensionProps2(params) {
|
|
6667
|
+
const { ctx, model, base, delegate } = params;
|
|
6668
|
+
const entry = entryOf(ctx, model);
|
|
6669
|
+
const out = {};
|
|
6670
|
+
for (const [key, prop] of Object.entries(base)) {
|
|
6671
|
+
if (key in delegate)
|
|
6672
|
+
continue;
|
|
6673
|
+
if (typeof prop === "function") {
|
|
6674
|
+
out[key] = (args) => {
|
|
6675
|
+
try {
|
|
6676
|
+
const path = `${model.clientName}.${key}`;
|
|
6677
|
+
const walked = isPlainObject5(args?.where) ? walkWhere({ ctx, model, where: args.where, path: `${path}.where` }) : args?.where;
|
|
6678
|
+
const where = entry.kind === "none" ? walked : mergeWhere2({ where: walked, pred: requirePred({ ctx, model, path }) });
|
|
6679
|
+
return prop({ ...args ?? {}, ...where === undefined ? {} : { where } });
|
|
6680
|
+
} catch (error) {
|
|
6681
|
+
return Promise.reject(error);
|
|
6682
|
+
}
|
|
6683
|
+
};
|
|
6684
|
+
} else if (isPlainObject5(prop)) {
|
|
6685
|
+
out[key] = scopeExtensionProps2({
|
|
6686
|
+
ctx,
|
|
6687
|
+
model,
|
|
6688
|
+
base: prop,
|
|
6689
|
+
delegate: {}
|
|
6690
|
+
});
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
return out;
|
|
6694
|
+
}
|
|
6695
|
+
function forbidden(params) {
|
|
6696
|
+
return refusal({
|
|
6697
|
+
model: "*",
|
|
6698
|
+
path: params.method,
|
|
6699
|
+
reason: "raw_forbidden",
|
|
6700
|
+
message: `$scoped: ${params.method} is not available on a scoped handle \u2014 raw SQL cannot be tenant-verified; run it on the BASE client (the deliberate unscoped escape)`
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
var RAW_METHODS = ["$queryRaw", "$executeRaw", "$queryRawUnsafe", "$executeRawUnsafe"];
|
|
6704
|
+
function buildScopedOver(params) {
|
|
6705
|
+
const { ctx, client } = params;
|
|
6706
|
+
const scoped = Object.create(client);
|
|
6707
|
+
const scopedModels = { ...client.$models };
|
|
6708
|
+
for (const model of ctx.meta.models) {
|
|
6709
|
+
const base = client.$models[model.name];
|
|
6710
|
+
const delegate = scopedDelegate2({ ctx, model, base });
|
|
6711
|
+
const full = { ...delegate, ...scopeExtensionProps2({ ctx, model, base, delegate }) };
|
|
6712
|
+
scopedModels[model.name] = full;
|
|
6713
|
+
scopedModels[model.clientName] = full;
|
|
6714
|
+
Object.defineProperty(scoped, model.clientName, { value: full, enumerable: true });
|
|
6715
|
+
}
|
|
6716
|
+
Object.defineProperty(scoped, "$models", { value: scopedModels, enumerable: true });
|
|
6717
|
+
attachDefineView({ client: scoped, meta: ctx.meta, models: scopedModels });
|
|
6718
|
+
Object.defineProperty(scoped, "$transaction", {
|
|
6719
|
+
value: (arg, options) => typeof arg === "function" ? client.$transaction((tx) => arg(buildScopedOver({ ctx, client: tx })), options) : client.$transaction(arg, options),
|
|
6720
|
+
enumerable: true
|
|
6721
|
+
});
|
|
6722
|
+
for (const method of RAW_METHODS) {
|
|
6723
|
+
Object.defineProperty(scoped, method, {
|
|
6724
|
+
value: () => Promise.reject(forbidden({ method })),
|
|
6725
|
+
enumerable: true
|
|
6726
|
+
});
|
|
6727
|
+
}
|
|
6728
|
+
Object.defineProperty(scoped, "$scoped", {
|
|
6729
|
+
value: () => {
|
|
6730
|
+
throw refusal({
|
|
6731
|
+
model: "*",
|
|
6732
|
+
path: "$scoped",
|
|
6733
|
+
reason: "rescope_forbidden",
|
|
6734
|
+
message: "$scoped: this handle is already scoped \u2014 build a differently-scoped handle from the BASE client"
|
|
6735
|
+
});
|
|
6736
|
+
},
|
|
6737
|
+
enumerable: true
|
|
6738
|
+
});
|
|
6739
|
+
return scoped;
|
|
6740
|
+
}
|
|
6741
|
+
function attachScoped(params) {
|
|
6742
|
+
const { client, meta } = params;
|
|
6743
|
+
Object.defineProperty(client, "$scoped", {
|
|
6744
|
+
value: (config) => {
|
|
6745
|
+
if (!isPlainObject5(config) || !isPlainObject5(config.models)) {
|
|
6746
|
+
throw new VibeError13({
|
|
6747
|
+
code: "VIBE_VALIDATION",
|
|
6748
|
+
message: "$scoped takes { scope: string | number | bigint | null, models: { <Model>: \u2026 } }"
|
|
6749
|
+
});
|
|
6750
|
+
}
|
|
6751
|
+
const policied = meta.models.filter((model) => model.policy !== undefined);
|
|
6752
|
+
if (policied.length > 0) {
|
|
6753
|
+
throw refusal({
|
|
6754
|
+
model: policied[0]?.name,
|
|
6755
|
+
path: "$scoped",
|
|
6756
|
+
reason: "policy_conflict",
|
|
6757
|
+
message: `$scoped: the schema declares @@policy on ${policied.map((model) => model.name).join(", ")} \u2014 use $withPolicy for policied schemas; the two scoping layers do not combine`
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
const scope = config.scope;
|
|
6761
|
+
if (scope !== null && typeof scope !== "string" && typeof scope !== "number" && typeof scope !== "bigint") {
|
|
6762
|
+
throw new VibeError13({
|
|
6763
|
+
code: "VIBE_VALIDATION",
|
|
6764
|
+
message: `$scoped: scope must be a string, number, bigint or null \u2014 got ${typeof scope}`
|
|
6765
|
+
});
|
|
6766
|
+
}
|
|
6767
|
+
const resolved = resolveScopedModels({ meta, models: config.models });
|
|
6768
|
+
if (scope !== null)
|
|
6769
|
+
assertScopeType({ meta, resolved, scope });
|
|
6770
|
+
const preds = scope === null ? new Map : buildPreds({ meta, resolved, scope });
|
|
6771
|
+
const ctx = { meta, resolved, scope, preds };
|
|
6772
|
+
return buildScopedOver({ ctx, client });
|
|
6773
|
+
},
|
|
6774
|
+
enumerable: true
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
5846
6777
|
// packages/runtime/src/client.ts
|
|
5847
|
-
import { VibeError as
|
|
6778
|
+
import { VibeError as VibeError14, unsupportedCapability as unsupportedCapability2 } from "@vibeorm/schema";
|
|
5848
6779
|
import { ParamCollector, getSqlDialect } from "@vibeorm/sql";
|
|
5849
6780
|
|
|
5850
6781
|
// packages/runtime/src/render-cache.ts
|
|
@@ -6031,7 +6962,7 @@ function walkExpr(walk, dialect, expr) {
|
|
|
6031
6962
|
return true;
|
|
6032
6963
|
}
|
|
6033
6964
|
}
|
|
6034
|
-
function
|
|
6965
|
+
function walkOrderTerm2(walk, dialect, term) {
|
|
6035
6966
|
if ("agg" in term) {
|
|
6036
6967
|
walk.key += "a";
|
|
6037
6968
|
addAgg(walk, term.agg);
|
|
@@ -6081,7 +7012,7 @@ function walkSelect(walk, dialect, statement) {
|
|
|
6081
7012
|
addCol(walk, column);
|
|
6082
7013
|
walk.key += ":";
|
|
6083
7014
|
for (const term of statement.rowNumber.orderBy) {
|
|
6084
|
-
if (!
|
|
7015
|
+
if (!walkOrderTerm2(walk, dialect, term))
|
|
6085
7016
|
return false;
|
|
6086
7017
|
}
|
|
6087
7018
|
addIdent(walk, statement.rowNumber.alias);
|
|
@@ -6136,7 +7067,7 @@ function walkSelect(walk, dialect, statement) {
|
|
|
6136
7067
|
if (statement.orderBy !== undefined && statement.orderBy.length > 0) {
|
|
6137
7068
|
walk.key += "o(";
|
|
6138
7069
|
for (const term of statement.orderBy) {
|
|
6139
|
-
if (!
|
|
7070
|
+
if (!walkOrderTerm2(walk, dialect, term))
|
|
6140
7071
|
return false;
|
|
6141
7072
|
}
|
|
6142
7073
|
walk.key += ")";
|
|
@@ -6294,7 +7225,7 @@ function renderStatementCached(params) {
|
|
|
6294
7225
|
// packages/runtime/src/client.ts
|
|
6295
7226
|
function notFound2(params) {
|
|
6296
7227
|
const { model, method } = params;
|
|
6297
|
-
return new
|
|
7228
|
+
return new VibeError14({
|
|
6298
7229
|
code: "VIBE_NOT_FOUND",
|
|
6299
7230
|
message: `no ${model} record matched the where clause of ${method}()`,
|
|
6300
7231
|
meta: { model, method }
|
|
@@ -6302,7 +7233,7 @@ function notFound2(params) {
|
|
|
6302
7233
|
}
|
|
6303
7234
|
function viewIsReadOnly(params) {
|
|
6304
7235
|
const { model, method } = params;
|
|
6305
|
-
return new
|
|
7236
|
+
return new VibeError14({
|
|
6306
7237
|
code: "VIBE_VALIDATION",
|
|
6307
7238
|
message: `${model} is a view \u2014 ${method}() is not available on views (they are read-only); write to the underlying table instead`,
|
|
6308
7239
|
meta: { model, method }
|
|
@@ -6431,7 +7362,7 @@ class QueryPromise extends Promise {
|
|
|
6431
7362
|
}
|
|
6432
7363
|
claim() {
|
|
6433
7364
|
if (this.started) {
|
|
6434
|
-
throw new
|
|
7365
|
+
throw new VibeError14({
|
|
6435
7366
|
code: "VIBE_VALIDATION",
|
|
6436
7367
|
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)"
|
|
6437
7368
|
});
|
|
@@ -6509,7 +7440,7 @@ async function runAffected(params) {
|
|
|
6509
7440
|
function plan(params) {
|
|
6510
7441
|
const { engine, model, method, args } = params;
|
|
6511
7442
|
if (args !== undefined && args["lock"] !== undefined && engine.inTransaction !== true) {
|
|
6512
|
-
throw new
|
|
7443
|
+
throw new VibeError14({
|
|
6513
7444
|
code: "VIBE_VALIDATION",
|
|
6514
7445
|
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)`,
|
|
6515
7446
|
meta: { model, method }
|
|
@@ -6582,7 +7513,7 @@ async function insertedKeyWhere(params) {
|
|
|
6582
7513
|
});
|
|
6583
7514
|
const id = result.rows[0]?.id;
|
|
6584
7515
|
if (id === undefined || id === null) {
|
|
6585
|
-
throw new
|
|
7516
|
+
throw new VibeError14({
|
|
6586
7517
|
code: "VIBE_INTERNAL",
|
|
6587
7518
|
message: `could not read back the generated key for ${source.model.name}`,
|
|
6588
7519
|
meta: { model: source.model.name }
|
|
@@ -6884,7 +7815,7 @@ function createDelegate(params) {
|
|
|
6884
7815
|
const skipping = args.onConflict !== undefined;
|
|
6885
7816
|
if (hasNestedWrites({ model: modelMeta, data: args.data })) {
|
|
6886
7817
|
if (skipping) {
|
|
6887
|
-
throw new
|
|
7818
|
+
throw new VibeError14({
|
|
6888
7819
|
code: "VIBE_VALIDATION",
|
|
6889
7820
|
message: `onConflict cannot combine with nested writes on ${modelMeta.name}.create() \u2014 insert the parent row alone, then connect relations`,
|
|
6890
7821
|
meta: { model: modelMeta.name }
|
|
@@ -6968,7 +7899,7 @@ function createDelegate(params) {
|
|
|
6968
7899
|
const upsert = async (target, args) => {
|
|
6969
7900
|
const modelMeta = getModelMeta({ meta: target.meta, model });
|
|
6970
7901
|
if (hasNestedWrites({ model: modelMeta, data: args.create }) || hasNestedWrites({ model: modelMeta, data: args.update })) {
|
|
6971
|
-
throw new
|
|
7902
|
+
throw new VibeError14({
|
|
6972
7903
|
code: "VIBE_VALIDATION",
|
|
6973
7904
|
message: `nested writes are not supported inside upsert arms on ${modelMeta.name} \u2014 use create()/update() with nested data`,
|
|
6974
7905
|
meta: { model: modelMeta.name }
|
|
@@ -6976,7 +7907,7 @@ function createDelegate(params) {
|
|
|
6976
7907
|
}
|
|
6977
7908
|
if (!upsertUsesNativeStatement({ model: modelMeta, args })) {
|
|
6978
7909
|
if (args.onConflict !== undefined) {
|
|
6979
|
-
throw new
|
|
7910
|
+
throw new VibeError14({
|
|
6980
7911
|
code: "VIBE_VALIDATION",
|
|
6981
7912
|
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`,
|
|
6982
7913
|
meta: { model: modelMeta.name }
|
|
@@ -7153,7 +8084,7 @@ function buildClient(params) {
|
|
|
7153
8084
|
let claimError = null;
|
|
7154
8085
|
for (const operation of operations) {
|
|
7155
8086
|
if (!(operation instanceof QueryPromise)) {
|
|
7156
|
-
claimError = new
|
|
8087
|
+
claimError = new VibeError14({
|
|
7157
8088
|
code: "VIBE_VALIDATION",
|
|
7158
8089
|
message: "$transaction([...]) takes query promises from model delegate calls \u2014 a foreign promise is already running and cannot join the transaction"
|
|
7159
8090
|
});
|
|
@@ -7192,7 +8123,7 @@ function buildClient(params) {
|
|
|
7192
8123
|
$models: models,
|
|
7193
8124
|
$on: (event, listener) => {
|
|
7194
8125
|
if (event !== "query") {
|
|
7195
|
-
throw new
|
|
8126
|
+
throw new VibeError14({
|
|
7196
8127
|
code: "VIBE_VALIDATION",
|
|
7197
8128
|
message: `unknown client event "${String(event)}" \u2014 supported: "query"`,
|
|
7198
8129
|
meta: { event }
|
|
@@ -7226,6 +8157,7 @@ function buildClient(params) {
|
|
|
7226
8157
|
}
|
|
7227
8158
|
attachExtensionMounts({ engine, client, models });
|
|
7228
8159
|
attachDefineView({ client, meta: engine.meta, models });
|
|
8160
|
+
attachScoped({ client, meta: engine.meta });
|
|
7229
8161
|
attachPolicy({ client, meta: engine.meta });
|
|
7230
8162
|
return client;
|
|
7231
8163
|
}
|
|
@@ -7256,7 +8188,7 @@ function makeMountContext(params) {
|
|
|
7256
8188
|
compileWhere: ({ where, model: modelName, qualify }) => {
|
|
7257
8189
|
const target = modelName === undefined ? model : getModelMeta({ meta: engine.meta, model: modelName });
|
|
7258
8190
|
if (target === null) {
|
|
7259
|
-
throw new
|
|
8191
|
+
throw new VibeError14({
|
|
7260
8192
|
code: "VIBE_INTERNAL",
|
|
7261
8193
|
message: `extension "${extension}" compiled a where with no model in scope`,
|
|
7262
8194
|
meta: { extension }
|
|
@@ -7282,7 +8214,7 @@ function attachExtensionMounts(params) {
|
|
|
7282
8214
|
const model = getModelMeta({ meta: engine.meta, model: mount.model ?? "" });
|
|
7283
8215
|
const delegate = models[model.name];
|
|
7284
8216
|
if (delegate === undefined || mount.property in delegate) {
|
|
7285
|
-
throw new
|
|
8217
|
+
throw new VibeError14({
|
|
7286
8218
|
code: "VIBE_CONFIG",
|
|
7287
8219
|
message: `extension "${mount.extension}" cannot mount "${mount.property}" on ${model.name} \u2014 the name is taken`,
|
|
7288
8220
|
meta: { extension: mount.extension, model: model.name, property: mount.property }
|
|
@@ -7293,7 +8225,7 @@ function attachExtensionMounts(params) {
|
|
|
7293
8225
|
continue;
|
|
7294
8226
|
}
|
|
7295
8227
|
if (!mount.property.startsWith("$") || mount.property in client) {
|
|
7296
|
-
throw new
|
|
8228
|
+
throw new VibeError14({
|
|
7297
8229
|
code: "VIBE_CONFIG",
|
|
7298
8230
|
message: `extension "${mount.extension}" cannot mount client property "${mount.property}" \u2014 root mounts are $-prefixed and must not collide`,
|
|
7299
8231
|
meta: { extension: mount.extension, property: mount.property }
|
|
@@ -7313,7 +8245,7 @@ function createClient(params) {
|
|
|
7313
8245
|
assertComputedManifest({ manifest: computed.manifest, specs: computed.specs });
|
|
7314
8246
|
}
|
|
7315
8247
|
if (computed === undefined && options !== undefined && options["computed"] !== undefined) {
|
|
7316
|
-
throw new
|
|
8248
|
+
throw new VibeError14({
|
|
7317
8249
|
code: "VIBE_CONFIG",
|
|
7318
8250
|
message: "this client was generated without computed fields \u2014 declare them in vibeorm.config.ts and re-run vibeorm generate",
|
|
7319
8251
|
meta: {}
|
|
@@ -7345,6 +8277,8 @@ export {
|
|
|
7345
8277
|
updateWithNested,
|
|
7346
8278
|
toSqlExecutor,
|
|
7347
8279
|
toClientName,
|
|
8280
|
+
rewriteScopedCall,
|
|
8281
|
+
resolveScopedModels,
|
|
7348
8282
|
resolveResultShape,
|
|
7349
8283
|
resolveRelationSelection,
|
|
7350
8284
|
resolveRelationLink,
|
|
@@ -7368,6 +8302,7 @@ export {
|
|
|
7368
8302
|
dbNow,
|
|
7369
8303
|
createWithNested,
|
|
7370
8304
|
createView,
|
|
8305
|
+
createScopeRewriter,
|
|
7371
8306
|
createClient,
|
|
7372
8307
|
compileWhere,
|
|
7373
8308
|
compileSelect,
|
|
@@ -7376,6 +8311,7 @@ export {
|
|
|
7376
8311
|
buildQuery,
|
|
7377
8312
|
buildExtensionOperatorMap,
|
|
7378
8313
|
buildConnectHooks,
|
|
8314
|
+
attachScoped,
|
|
7379
8315
|
attachRelations,
|
|
7380
8316
|
attachPolicy,
|
|
7381
8317
|
attachLateralRelations,
|
|
@@ -7392,4 +8328,4 @@ export {
|
|
|
7392
8328
|
COUNT_ALIAS
|
|
7393
8329
|
};
|
|
7394
8330
|
|
|
7395
|
-
//# debugId=
|
|
8331
|
+
//# debugId=AB87792190B87F3664756E2164756E21
|