@ganju/utils 0.0.15 → 0.0.17
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/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +107 -9
- package/dist/customCodeToken.d.ts +11 -0
- package/dist/customCodeToken.d.ts.map +1 -1
- package/dist/customCodeToken.js +41 -1
- package/dist/data.d.ts +13 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +103 -0
- package/dist/dataApi.d.ts +578 -0
- package/dist/dataApi.d.ts.map +1 -0
- package/dist/dataApi.js +399 -0
- package/dist/dataCompiler.d.ts +68 -0
- package/dist/dataCompiler.d.ts.map +1 -0
- package/dist/dataCompiler.js +730 -0
- package/dist/dataCsv.d.ts +20 -0
- package/dist/dataCsv.d.ts.map +1 -0
- package/dist/dataCsv.js +102 -0
- package/dist/dataDdl.d.ts +29 -0
- package/dist/dataDdl.d.ts.map +1 -0
- package/dist/dataDdl.js +354 -0
- package/dist/dataEngine.d.ts +143 -0
- package/dist/dataEngine.d.ts.map +1 -0
- package/dist/dataEngine.js +2042 -0
- package/dist/dataIdentifiers.d.ts +8 -0
- package/dist/dataIdentifiers.d.ts.map +1 -0
- package/dist/dataIdentifiers.js +55 -0
- package/dist/dataImport.d.ts +79 -0
- package/dist/dataImport.d.ts.map +1 -0
- package/dist/dataImport.js +453 -0
- package/dist/dataQueries.d.ts +73 -0
- package/dist/dataQueries.d.ts.map +1 -0
- package/dist/dataQueries.js +513 -0
- package/dist/dataRpc.d.ts +150 -0
- package/dist/dataRpc.d.ts.map +1 -0
- package/dist/dataRpc.js +2 -0
- package/dist/dataTools.d.ts +40 -0
- package/dist/dataTools.d.ts.map +1 -0
- package/dist/dataTools.js +280 -0
- package/dist/dataTypes.d.ts +159 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +160 -0
- package/dist/index.d.ts +394 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/oauth.d.ts +14 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +42 -2
- package/dist/resourceRerank.d.ts +3 -2
- package/dist/resourceRerank.d.ts.map +1 -1
- package/dist/resourceRerank.js +5 -2
- package/dist/schema.d.ts +376 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +268 -1
- package/dist/sdkConstants.d.ts +1 -0
- package/dist/sdkConstants.d.ts.map +1 -1
- package/dist/sdkConstants.js +2 -1
- package/package.json +17 -2
- package/src/constants.ts +121 -9
- package/src/customCodeToken.ts +74 -0
- package/src/data.ts +108 -0
- package/src/dataApi.ts +479 -0
- package/src/dataCompiler.ts +952 -0
- package/src/dataCsv.ts +99 -0
- package/src/dataDdl.ts +419 -0
- package/src/dataEngine.ts +2773 -0
- package/src/dataIdentifiers.ts +63 -0
- package/src/dataImport.ts +557 -0
- package/src/dataQueries.ts +730 -0
- package/src/dataRpc.ts +132 -0
- package/src/dataTools.ts +338 -0
- package/src/dataTypes.ts +318 -0
- package/src/index.ts +13 -1
- package/src/oauth.ts +42 -1
- package/src/resourceRerank.ts +5 -2
- package/src/schema.ts +315 -1
- package/src/sdkConstants.ts +1 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSensitiveWrite = exports.tidyArgs = exports.chunkRows = exports.rowsPerInsert = exports.keysetCondition = exports.decodeCursor = exports.encodeCursor = exports.ftsMatch = exports.assertParamCount = exports.compileWhere = exports.resolvePath = exports.fromStored = exports.zoned = exports.zoneOffsetMinutes = exports.toStored = exports.isoSeconds = exports.relationJoin = exports.joinsParams = exports.joinsSql = exports.participantOf = exports.canRead = exports.isScoped = exports.tableFor = exports.visibleTables = exports.accessOf = exports.unknownColumn = exports.columnFor = exports.isSystemColumn = exports.FILTER_OPERATORS = void 0;
|
|
4
|
+
// Turns the structured query and write arguments into SQL with bound values.
|
|
5
|
+
// Identifiers are checked against the database's own schema and quoted;
|
|
6
|
+
// every value a caller sends is a parameter. Pure: the engine runs the SQL.
|
|
7
|
+
const dataIdentifiers_1 = require("./dataIdentifiers");
|
|
8
|
+
const dataDdl_1 = require("./dataDdl");
|
|
9
|
+
const base64_1 = require("./base64");
|
|
10
|
+
const dataTypes_1 = require("./dataTypes");
|
|
11
|
+
exports.FILTER_OPERATORS = [
|
|
12
|
+
'eq',
|
|
13
|
+
'ne',
|
|
14
|
+
'gt',
|
|
15
|
+
'gte',
|
|
16
|
+
'lt',
|
|
17
|
+
'lte',
|
|
18
|
+
'in',
|
|
19
|
+
'notIn',
|
|
20
|
+
'contains',
|
|
21
|
+
'startsWith',
|
|
22
|
+
'isNull'
|
|
23
|
+
];
|
|
24
|
+
const SYSTEM_COLUMN_TYPES = {
|
|
25
|
+
id: 'integer',
|
|
26
|
+
created_at: 'datetime',
|
|
27
|
+
updated_at: 'datetime'
|
|
28
|
+
};
|
|
29
|
+
const isSystemColumn = (column) => 'system' in column && column.system === true;
|
|
30
|
+
exports.isSystemColumn = isSystemColumn;
|
|
31
|
+
const columnFor = (table, name) => {
|
|
32
|
+
if (name in SYSTEM_COLUMN_TYPES) {
|
|
33
|
+
return { name, type: SYSTEM_COLUMN_TYPES[name], system: true };
|
|
34
|
+
}
|
|
35
|
+
return table.columns.find(column => column.name === name);
|
|
36
|
+
};
|
|
37
|
+
exports.columnFor = columnFor;
|
|
38
|
+
const unknownColumn = (table, name) => new dataTypes_1.DataError(`Unknown column "${name}" in ${table.name}. Columns: ${[
|
|
39
|
+
'id',
|
|
40
|
+
...table.columns.map(c => c.name),
|
|
41
|
+
'created_at',
|
|
42
|
+
'updated_at'
|
|
43
|
+
].join(', ')}.`, 'invalid');
|
|
44
|
+
exports.unknownColumn = unknownColumn;
|
|
45
|
+
// Access
|
|
46
|
+
const accessOf = (caller, table) => caller.tables && !caller.tables.includes(table.name)
|
|
47
|
+
? 'none'
|
|
48
|
+
: caller.restricted
|
|
49
|
+
? table.access
|
|
50
|
+
: 'full';
|
|
51
|
+
exports.accessOf = accessOf;
|
|
52
|
+
const visibleTables = (schema, caller) => schema.tables.filter(table => dataTypes_1.DATA_ACCESS_RANK[(0, exports.accessOf)(caller, table)] >= 1);
|
|
53
|
+
exports.visibleTables = visibleTables;
|
|
54
|
+
// The table a call names, if the caller may `need` it. A table the caller
|
|
55
|
+
// can't see is reported as unknown, so a hidden table doesn't exist for it.
|
|
56
|
+
const tableFor = (schema, caller, name, need = 'read') => {
|
|
57
|
+
const table = typeof name === 'string'
|
|
58
|
+
? schema.tables.find(t => t.name === name)
|
|
59
|
+
: undefined;
|
|
60
|
+
if (!table || dataTypes_1.DATA_ACCESS_RANK[(0, exports.accessOf)(caller, table)] < 1) {
|
|
61
|
+
const names = (0, exports.visibleTables)(schema, caller).map(t => t.name);
|
|
62
|
+
throw new dataTypes_1.DataError(typeof name === 'string'
|
|
63
|
+
? `Unknown table "${name}". Tables: ${names.join(', ') || '(none)'}.`
|
|
64
|
+
: `"table" is the name of a table. Tables: ${names.join(', ') || '(none)'}.`, 'not_found');
|
|
65
|
+
}
|
|
66
|
+
const access = (0, exports.accessOf)(caller, table);
|
|
67
|
+
if (dataTypes_1.DATA_ACCESS_RANK[access] < dataTypes_1.DATA_ACCESS_RANK[need]) {
|
|
68
|
+
const allowed = access === 'read'
|
|
69
|
+
? 'read only'
|
|
70
|
+
: access === 'add'
|
|
71
|
+
? 'read and add rows only'
|
|
72
|
+
: access;
|
|
73
|
+
throw new dataTypes_1.DataError(`Not allowed: the assistant's access to "${table.name}" is ${allowed}.`, 'forbidden');
|
|
74
|
+
}
|
|
75
|
+
return table;
|
|
76
|
+
};
|
|
77
|
+
exports.tableFor = tableFor;
|
|
78
|
+
const isScoped = (caller, table) => Boolean(caller.restricted && table.customerScoped);
|
|
79
|
+
exports.isScoped = isScoped;
|
|
80
|
+
const canRead = (caller, table) => dataTypes_1.DATA_ACCESS_RANK[(0, exports.accessOf)(caller, table)] >= 1;
|
|
81
|
+
exports.canRead = canRead;
|
|
82
|
+
// The customer a scoped call is for. A restricted caller without one would
|
|
83
|
+
// otherwise share a single bucket with every other caller like it.
|
|
84
|
+
const participantOf = (caller) => {
|
|
85
|
+
if (typeof caller.participantId !== 'string' || !caller.participantId) {
|
|
86
|
+
throw new dataTypes_1.DataError("This conversation has no customer, so customer-scoped tables can't be used.", 'forbidden');
|
|
87
|
+
}
|
|
88
|
+
return caller.participantId;
|
|
89
|
+
};
|
|
90
|
+
exports.participantOf = participantOf;
|
|
91
|
+
const joinsSql = (joins) => [...joins.values()].map(join => join.sql).join(' ');
|
|
92
|
+
exports.joinsSql = joinsSql;
|
|
93
|
+
const joinsParams = (joins) => [...joins.values()].flatMap(join => join.params);
|
|
94
|
+
exports.joinsParams = joinsParams;
|
|
95
|
+
// The related table as the caller may see it: a bot reaches only its own
|
|
96
|
+
// customer's rows of a scoped table, through a label, a filter or a sort just
|
|
97
|
+
// as through a query on that table.
|
|
98
|
+
const relationJoin = (caller, target, alias, on) => {
|
|
99
|
+
const scoped = (0, exports.isScoped)(caller, target);
|
|
100
|
+
// No customer: no row of a scoped table is theirs.
|
|
101
|
+
if (scoped && !caller.participantId) {
|
|
102
|
+
return { sql: `LEFT JOIN ${(0, dataIdentifiers_1.quoteIdentifier)(target.name)} ${alias} ON 0`, params: [] };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
sql: `LEFT JOIN ${(0, dataIdentifiers_1.quoteIdentifier)(target.name)} ${alias} ON ${alias}.id = ${on}${scoped ? ` AND ${alias}.participant_id = ?` : ''}`,
|
|
106
|
+
params: scoped ? [(0, exports.participantOf)(caller)] : []
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
exports.relationJoin = relationJoin;
|
|
110
|
+
// Values
|
|
111
|
+
const isoSeconds = (date) => date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
112
|
+
exports.isoSeconds = isoSeconds;
|
|
113
|
+
const LOCAL_DATETIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?$/;
|
|
114
|
+
// "2026-10-21T15:00:00" with no offset, read as a wall-clock time in
|
|
115
|
+
// `timeZone` (UTC without one). `new Date()` would read it in the server's
|
|
116
|
+
// zone: UTC on Workers, the developer's own zone on a laptop.
|
|
117
|
+
const localDatetime = (value, timeZone) => {
|
|
118
|
+
const m = LOCAL_DATETIME.exec(value.trim());
|
|
119
|
+
if (!m)
|
|
120
|
+
return null;
|
|
121
|
+
const [year, month, day, hour, minute, second] = m
|
|
122
|
+
.slice(1, 7)
|
|
123
|
+
.map(part => Number(part ?? 0));
|
|
124
|
+
const wall = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
125
|
+
const check = new Date(wall);
|
|
126
|
+
if (check.getUTCFullYear() !== year ||
|
|
127
|
+
check.getUTCMonth() !== month - 1 ||
|
|
128
|
+
check.getUTCDate() !== day ||
|
|
129
|
+
hour > 23 ||
|
|
130
|
+
minute > 59 ||
|
|
131
|
+
second > 59) {
|
|
132
|
+
return new Date(NaN);
|
|
133
|
+
}
|
|
134
|
+
// The offset at the guess, then again at the result, so a time next to a
|
|
135
|
+
// daylight-saving change lands on the right side of it.
|
|
136
|
+
let at = wall - (0, exports.zoneOffsetMinutes)(new Date(wall), timeZone) * 60_000;
|
|
137
|
+
at = wall - (0, exports.zoneOffsetMinutes)(new Date(at), timeZone) * 60_000;
|
|
138
|
+
return new Date(at);
|
|
139
|
+
};
|
|
140
|
+
const normaliseDatetime = (value, label, timeZone) => {
|
|
141
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
142
|
+
throw new dataTypes_1.DataError(`${label}: expected an ISO 8601 date and time.`);
|
|
143
|
+
}
|
|
144
|
+
const date = (typeof value === 'string' ? localDatetime(value, timeZone) : null) ??
|
|
145
|
+
new Date(value);
|
|
146
|
+
if (Number.isNaN(date.getTime())) {
|
|
147
|
+
throw new dataTypes_1.DataError(`${label}: "${value}" is not a date and time; use ISO 8601, e.g. 2026-10-19T09:00:00-05:00.`);
|
|
148
|
+
}
|
|
149
|
+
return (0, exports.isoSeconds)(date);
|
|
150
|
+
};
|
|
151
|
+
const isRealDate = (value) => {
|
|
152
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
153
|
+
return (!Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value);
|
|
154
|
+
};
|
|
155
|
+
const asNumber = (value) => {
|
|
156
|
+
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
157
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
|
158
|
+
};
|
|
159
|
+
const checkSize = (label, value) => {
|
|
160
|
+
if (value.length > dataTypes_1.DATA_MAX_VALUE_BYTES) {
|
|
161
|
+
throw new dataTypes_1.DataError(`${label} is too long (over ${dataTypes_1.DATA_MAX_VALUE_BYTES.toLocaleString('en-US')} characters).`, 'too_large');
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
};
|
|
165
|
+
// Turns a value from a caller into what the column stores, or explains why
|
|
166
|
+
// it can't. `timeZone` is the project's, for datetimes sent without an
|
|
167
|
+
// offset.
|
|
168
|
+
const toStored = (table, column, value, timeZone) => {
|
|
169
|
+
const label = `${table.name}.${column.name}`;
|
|
170
|
+
if (value === null || value === undefined) {
|
|
171
|
+
if (!(0, exports.isSystemColumn)(column) && column.required) {
|
|
172
|
+
throw new dataTypes_1.DataError(`${label} is required.`);
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
switch (column.type) {
|
|
177
|
+
case 'integer':
|
|
178
|
+
case 'relation': {
|
|
179
|
+
const n = typeof value === 'string' && /^-?\d+$/.test(value.trim())
|
|
180
|
+
? Number(value)
|
|
181
|
+
: value;
|
|
182
|
+
if (typeof n !== 'number' || !Number.isSafeInteger(n)) {
|
|
183
|
+
const target = (0, exports.isSystemColumn)(column) ? null : column.table;
|
|
184
|
+
throw new dataTypes_1.DataError(column.type === 'relation'
|
|
185
|
+
? `${label} takes the id (a whole number) of a row in ${target}.`
|
|
186
|
+
: `${label} must be a whole number.`);
|
|
187
|
+
}
|
|
188
|
+
return n;
|
|
189
|
+
}
|
|
190
|
+
case 'decimal': {
|
|
191
|
+
const n = asNumber(value);
|
|
192
|
+
if (n === undefined)
|
|
193
|
+
throw new dataTypes_1.DataError(`${label} must be a number.`);
|
|
194
|
+
return n;
|
|
195
|
+
}
|
|
196
|
+
case 'boolean':
|
|
197
|
+
if (value === true || value === 'true' || value === 1)
|
|
198
|
+
return 1;
|
|
199
|
+
if (value === false || value === 'false' || value === 0)
|
|
200
|
+
return 0;
|
|
201
|
+
throw new dataTypes_1.DataError(`${label} must be true or false.`);
|
|
202
|
+
case 'date':
|
|
203
|
+
if (typeof value !== 'string' ||
|
|
204
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(value) ||
|
|
205
|
+
!isRealDate(value)) {
|
|
206
|
+
throw new dataTypes_1.DataError(`${label} must be a date, YYYY-MM-DD.`);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
case 'datetime':
|
|
210
|
+
return normaliseDatetime(value, label, timeZone);
|
|
211
|
+
case 'enum': {
|
|
212
|
+
const values = (0, exports.isSystemColumn)(column) ? [] : (column.values ?? []);
|
|
213
|
+
const match = values.find(v => v.toLowerCase() === String(value).toLowerCase());
|
|
214
|
+
if (!match) {
|
|
215
|
+
throw new dataTypes_1.DataError(`${label} must be one of: ${values.join(', ')}.`);
|
|
216
|
+
}
|
|
217
|
+
return match;
|
|
218
|
+
}
|
|
219
|
+
case 'json':
|
|
220
|
+
return checkSize(label, JSON.stringify(value));
|
|
221
|
+
case 'email': {
|
|
222
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
223
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(text)) {
|
|
224
|
+
throw new dataTypes_1.DataError(`${label} must be an email address.`);
|
|
225
|
+
}
|
|
226
|
+
return checkSize(label, text);
|
|
227
|
+
}
|
|
228
|
+
case 'url':
|
|
229
|
+
if (typeof value !== 'string' || !/^https?:\/\/\S+$/i.test(value.trim())) {
|
|
230
|
+
throw new dataTypes_1.DataError(`${label} must be a URL starting with http(s)://.`);
|
|
231
|
+
}
|
|
232
|
+
return checkSize(label, value.trim());
|
|
233
|
+
case 'phone': {
|
|
234
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
235
|
+
throw new dataTypes_1.DataError(`${label} must be a phone number.`);
|
|
236
|
+
}
|
|
237
|
+
const phone = String(value).replace(/[^\d+]/g, '');
|
|
238
|
+
if (!/^\+?\d{4,20}$/.test(phone)) {
|
|
239
|
+
throw new dataTypes_1.DataError(`${label} must be a phone number.`);
|
|
240
|
+
}
|
|
241
|
+
return phone;
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
if (typeof value === 'object') {
|
|
245
|
+
throw new dataTypes_1.DataError(`${label} must be text.`);
|
|
246
|
+
}
|
|
247
|
+
return checkSize(label, String(value));
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
exports.toStored = toStored;
|
|
251
|
+
const zoneOffsetMinutes = (date, timeZone) => {
|
|
252
|
+
if (!timeZone)
|
|
253
|
+
return 0;
|
|
254
|
+
const parts = {};
|
|
255
|
+
for (const part of new Intl.DateTimeFormat('en-US', {
|
|
256
|
+
timeZone,
|
|
257
|
+
hourCycle: 'h23',
|
|
258
|
+
year: 'numeric',
|
|
259
|
+
month: '2-digit',
|
|
260
|
+
day: '2-digit',
|
|
261
|
+
hour: '2-digit',
|
|
262
|
+
minute: '2-digit',
|
|
263
|
+
second: '2-digit'
|
|
264
|
+
}).formatToParts(date)) {
|
|
265
|
+
parts[part.type] = part.value;
|
|
266
|
+
}
|
|
267
|
+
const asUtc = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour), Number(parts.minute), Number(parts.second));
|
|
268
|
+
return Math.round((asUtc - date.getTime()) / 60_000);
|
|
269
|
+
};
|
|
270
|
+
exports.zoneOffsetMinutes = zoneOffsetMinutes;
|
|
271
|
+
// A stored UTC datetime in the caller's zone, with the offset. Given UTC,
|
|
272
|
+
// models told customers "8:00 p. m." for a 3 pm appointment.
|
|
273
|
+
const zoned = (iso, timeZone) => {
|
|
274
|
+
const date = new Date(iso);
|
|
275
|
+
if (!timeZone || Number.isNaN(date.getTime()))
|
|
276
|
+
return iso;
|
|
277
|
+
const offset = (0, exports.zoneOffsetMinutes)(date, timeZone);
|
|
278
|
+
const local = new Date(date.getTime() + offset * 60_000)
|
|
279
|
+
.toISOString()
|
|
280
|
+
.slice(0, 19);
|
|
281
|
+
const sign = offset < 0 ? '-' : '+';
|
|
282
|
+
const abs = Math.abs(offset);
|
|
283
|
+
return `${local}${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
|
|
284
|
+
};
|
|
285
|
+
exports.zoned = zoned;
|
|
286
|
+
const fromStored = (column, value, timeZone) => {
|
|
287
|
+
if (value === null || value === undefined)
|
|
288
|
+
return null;
|
|
289
|
+
if (column.type === 'datetime')
|
|
290
|
+
return (0, exports.zoned)(String(value), timeZone);
|
|
291
|
+
if (column.type === 'boolean')
|
|
292
|
+
return value === 1;
|
|
293
|
+
if (column.type === 'json') {
|
|
294
|
+
try {
|
|
295
|
+
return JSON.parse(String(value));
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return value;
|
|
302
|
+
};
|
|
303
|
+
exports.fromStored = fromStored;
|
|
304
|
+
// "column" or "relation.column", adding the join a relation path needs.
|
|
305
|
+
const resolvePath = (ctx, path) => {
|
|
306
|
+
if (typeof path !== 'string') {
|
|
307
|
+
throw new dataTypes_1.DataError('Column names are strings.');
|
|
308
|
+
}
|
|
309
|
+
const [head, tail, ...rest] = path.split('.');
|
|
310
|
+
if (rest.length > 0) {
|
|
311
|
+
throw new dataTypes_1.DataError(`"${path}": only one level of relation is supported (relation.column).`);
|
|
312
|
+
}
|
|
313
|
+
const column = (0, exports.columnFor)(ctx.table, head);
|
|
314
|
+
if (!column)
|
|
315
|
+
throw (0, exports.unknownColumn)(ctx.table, head);
|
|
316
|
+
if (tail === undefined) {
|
|
317
|
+
return { sql: `t.${(0, dataIdentifiers_1.quoteIdentifier)(head)}`, column, table: ctx.table, own: true };
|
|
318
|
+
}
|
|
319
|
+
if ((0, exports.isSystemColumn)(column) || column.type !== 'relation' || !column.table) {
|
|
320
|
+
throw new dataTypes_1.DataError(`"${head}" is not a relation, so "${path}" can't be used.`);
|
|
321
|
+
}
|
|
322
|
+
const target = (0, exports.tableFor)(ctx.schema, ctx.caller, column.table);
|
|
323
|
+
const targetColumn = (0, exports.columnFor)(target, tail);
|
|
324
|
+
if (!targetColumn)
|
|
325
|
+
throw (0, exports.unknownColumn)(target, tail);
|
|
326
|
+
const alias = `j_${head}`;
|
|
327
|
+
ctx.joins.set(alias, (0, exports.relationJoin)(ctx.caller, target, alias, `t.${(0, dataIdentifiers_1.quoteIdentifier)(head)}`));
|
|
328
|
+
return {
|
|
329
|
+
sql: `${alias}.${(0, dataIdentifiers_1.quoteIdentifier)(tail)}`,
|
|
330
|
+
column: targetColumn,
|
|
331
|
+
table: target,
|
|
332
|
+
own: false
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
exports.resolvePath = resolvePath;
|
|
336
|
+
const filterValue = (resolved, value, timeZone) => {
|
|
337
|
+
const { column, table } = resolved;
|
|
338
|
+
const label = `${table.name}.${column.name}`;
|
|
339
|
+
if (column.type === 'datetime') {
|
|
340
|
+
return normaliseDatetime(value, label, timeZone);
|
|
341
|
+
}
|
|
342
|
+
if (column.type === 'boolean' || column.type === 'enum') {
|
|
343
|
+
if (column.type === 'enum') {
|
|
344
|
+
const values = (0, exports.isSystemColumn)(column) ? [] : (column.values ?? []);
|
|
345
|
+
const match = values.find(v => v.toLowerCase() === String(value).toLowerCase());
|
|
346
|
+
if (!match) {
|
|
347
|
+
throw new dataTypes_1.DataError(`${label} is one of: ${values.join(', ')}.`);
|
|
348
|
+
}
|
|
349
|
+
return match;
|
|
350
|
+
}
|
|
351
|
+
return (0, exports.toStored)(table, column, value);
|
|
352
|
+
}
|
|
353
|
+
if (dataDdl_1.NUMERIC_TYPES.has(column.type) || column.type === 'relation') {
|
|
354
|
+
const n = asNumber(value);
|
|
355
|
+
if (n === undefined) {
|
|
356
|
+
throw new dataTypes_1.DataError(`${label} compares with a number.`);
|
|
357
|
+
}
|
|
358
|
+
return n;
|
|
359
|
+
}
|
|
360
|
+
if (typeof value === 'object' && value !== null) {
|
|
361
|
+
throw new dataTypes_1.DataError(`${label}: expected a value or an operator object.`);
|
|
362
|
+
}
|
|
363
|
+
if (typeof value === 'boolean')
|
|
364
|
+
return String(value);
|
|
365
|
+
return value;
|
|
366
|
+
};
|
|
367
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
368
|
+
// The conditions of a `where`, as SQL joined with AND by the caller.
|
|
369
|
+
// `indexed` collects the columns of the queried table an AND-ed condition
|
|
370
|
+
// filters on, which is what lets a large table be aggregated.
|
|
371
|
+
const compileWhere = (ctx, where, indexed) => {
|
|
372
|
+
if (where === undefined || where === null)
|
|
373
|
+
return [];
|
|
374
|
+
if (!isPlainObject(where)) {
|
|
375
|
+
throw new dataTypes_1.DataError('"where" is an object: { column: value, column: { operator: value } }.');
|
|
376
|
+
}
|
|
377
|
+
const clauses = [];
|
|
378
|
+
for (const [key, condition] of Object.entries(where)) {
|
|
379
|
+
if (key === 'or' || key === 'and') {
|
|
380
|
+
if (!Array.isArray(condition) || condition.length === 0) {
|
|
381
|
+
throw new dataTypes_1.DataError(`"${key}" takes a list of where objects.`);
|
|
382
|
+
}
|
|
383
|
+
const parts = condition.map(sub => {
|
|
384
|
+
const inner = (0, exports.compileWhere)(ctx, sub, key === 'and' ? indexed : undefined);
|
|
385
|
+
// An empty entry would match every row: { "or": [{}] } is "1".
|
|
386
|
+
if (inner.length === 0) {
|
|
387
|
+
throw new dataTypes_1.DataError(`Each entry in "${key}" is a where object with at least one condition.`);
|
|
388
|
+
}
|
|
389
|
+
return `(${inner.join(' AND ')})`;
|
|
390
|
+
});
|
|
391
|
+
clauses.push(`(${parts.join(key === 'or' ? ' OR ' : ' AND ')})`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const resolved = (0, exports.resolvePath)(ctx, key);
|
|
395
|
+
// { "customer": { "name": "Ana" } } reads as "customer.name": models
|
|
396
|
+
// write relation filters this way unprompted.
|
|
397
|
+
if (resolved.column.type === 'relation' &&
|
|
398
|
+
!key.includes('.') &&
|
|
399
|
+
isPlainObject(condition) &&
|
|
400
|
+
Object.keys(condition).some(k => !exports.FILTER_OPERATORS.includes(k))) {
|
|
401
|
+
clauses.push(...(0, exports.compileWhere)(ctx, Object.fromEntries(Object.entries(condition).map(([k, v]) => [`${key}.${k}`, v]))));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (resolved.own && indexed) {
|
|
405
|
+
const column = resolved.column;
|
|
406
|
+
if (column.name === 'id' ||
|
|
407
|
+
(!(0, exports.isSystemColumn)(column) &&
|
|
408
|
+
(column.unique ||
|
|
409
|
+
column.filterable ||
|
|
410
|
+
column.type === 'relation' ||
|
|
411
|
+
column.type === 'file'))) {
|
|
412
|
+
indexed.add(column.name);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!isPlainObject(condition) || resolved.column.type === 'json') {
|
|
416
|
+
if (Array.isArray(condition)) {
|
|
417
|
+
throw new dataTypes_1.DataError(`${key}: to match several values use { "in": [...] }.`);
|
|
418
|
+
}
|
|
419
|
+
if (condition === null) {
|
|
420
|
+
clauses.push(`${resolved.sql} IS NULL`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
clauses.push(`${resolved.sql} = ?`);
|
|
424
|
+
ctx.params.push(resolved.column.type === 'json'
|
|
425
|
+
? JSON.stringify(condition)
|
|
426
|
+
: filterValue(resolved, condition, ctx.caller.timeZone));
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
for (const [operator, value] of Object.entries(condition)) {
|
|
431
|
+
if (!exports.FILTER_OPERATORS.includes(operator)) {
|
|
432
|
+
throw new dataTypes_1.DataError(`${key}: unknown operator "${operator}". Operators: ${exports.FILTER_OPERATORS.join(', ')}.`);
|
|
433
|
+
}
|
|
434
|
+
switch (operator) {
|
|
435
|
+
case 'eq':
|
|
436
|
+
case 'ne':
|
|
437
|
+
case 'gt':
|
|
438
|
+
case 'gte':
|
|
439
|
+
case 'lt':
|
|
440
|
+
case 'lte': {
|
|
441
|
+
if (value === null && (operator === 'eq' || operator === 'ne')) {
|
|
442
|
+
clauses.push(`${resolved.sql} IS ${operator === 'ne' ? 'NOT ' : ''}NULL`);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
const sign = {
|
|
446
|
+
eq: '=',
|
|
447
|
+
ne: '!=',
|
|
448
|
+
gt: '>',
|
|
449
|
+
gte: '>=',
|
|
450
|
+
lt: '<',
|
|
451
|
+
lte: '<='
|
|
452
|
+
}[operator];
|
|
453
|
+
// { "stock": { "lt": { "column": "reorder_level" } } }
|
|
454
|
+
if (isPlainObject(value) && typeof value.column === 'string') {
|
|
455
|
+
const other = (0, exports.resolvePath)(ctx, value.column);
|
|
456
|
+
clauses.push(`${resolved.sql} ${sign} ${other.sql}`);
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
clauses.push(`${resolved.sql} ${sign} ?`);
|
|
460
|
+
ctx.params.push(filterValue(resolved, value, ctx.caller.timeZone));
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case 'in':
|
|
464
|
+
case 'notIn': {
|
|
465
|
+
if (!Array.isArray(value) ||
|
|
466
|
+
value.length === 0 ||
|
|
467
|
+
value.length > dataTypes_1.DATA_MAX_IN_VALUES) {
|
|
468
|
+
throw new dataTypes_1.DataError(`${key}.${operator} takes a list of 1 to ${dataTypes_1.DATA_MAX_IN_VALUES} values.`);
|
|
469
|
+
}
|
|
470
|
+
clauses.push(`${resolved.sql} ${operator === 'notIn' ? 'NOT ' : ''}IN (${value
|
|
471
|
+
.map(() => '?')
|
|
472
|
+
.join(', ')})`);
|
|
473
|
+
ctx.params.push(...value.map(v => filterValue(resolved, v, ctx.caller.timeZone)));
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case 'contains':
|
|
477
|
+
case 'startsWith': {
|
|
478
|
+
if (typeof value !== 'string') {
|
|
479
|
+
throw new dataTypes_1.DataError(`${key}.${operator} takes text.`);
|
|
480
|
+
}
|
|
481
|
+
const escaped = value.replace(/[\\%_]/g, c => `\\${c}`);
|
|
482
|
+
clauses.push(`${resolved.sql} LIKE ? ESCAPE '\\'`);
|
|
483
|
+
ctx.params.push(operator === 'contains' ? `%${escaped}%` : `${escaped}%`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case 'isNull':
|
|
487
|
+
clauses.push(`${resolved.sql} IS ${value ? '' : 'NOT '}NULL`);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return clauses;
|
|
493
|
+
};
|
|
494
|
+
exports.compileWhere = compileWhere;
|
|
495
|
+
const assertParamCount = (params) => {
|
|
496
|
+
if (params.length > dataTypes_1.DATA_MAX_PARAMS) {
|
|
497
|
+
throw new dataTypes_1.DataError(`This query uses ${params.length} values; the limit is ${dataTypes_1.DATA_MAX_PARAMS}. Use fewer "in" values or split it.`, 'too_large');
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
exports.assertParamCount = assertParamCount;
|
|
501
|
+
// Search
|
|
502
|
+
// FTS5 query for the words in `text`: each word as a quoted prefix, so
|
|
503
|
+
// nothing the caller types is FTS syntax. All words, or any with `any`.
|
|
504
|
+
const ftsMatch = (text, any) => {
|
|
505
|
+
const tokens = String(text ?? '')
|
|
506
|
+
.normalize('NFKD')
|
|
507
|
+
.replace(/[̀-ͯ]/g, '')
|
|
508
|
+
.toLowerCase()
|
|
509
|
+
.match(/[\p{L}\p{N}]+/gu);
|
|
510
|
+
if (!tokens || tokens.length === 0) {
|
|
511
|
+
throw new dataTypes_1.DataError('"search" needs at least one word.');
|
|
512
|
+
}
|
|
513
|
+
return tokens
|
|
514
|
+
.slice(0, 10)
|
|
515
|
+
.map(t => `"${t}"*`)
|
|
516
|
+
.join(any ? ' OR ' : ' ');
|
|
517
|
+
};
|
|
518
|
+
exports.ftsMatch = ftsMatch;
|
|
519
|
+
const encodeCursor = (cursor) => (0, base64_1.utf8ToBase64Url)(JSON.stringify(cursor));
|
|
520
|
+
exports.encodeCursor = encodeCursor;
|
|
521
|
+
const decodeCursor = (raw, signature) => {
|
|
522
|
+
try {
|
|
523
|
+
const cursor = JSON.parse((0, base64_1.base64UrlToUtf8)(String(raw)));
|
|
524
|
+
if (!cursor ||
|
|
525
|
+
cursor.k !== signature ||
|
|
526
|
+
!Array.isArray(cursor.v) ||
|
|
527
|
+
cursor.v.some(v => v !== null && typeof v !== 'string' && typeof v !== 'number')) {
|
|
528
|
+
throw new Error('mismatch');
|
|
529
|
+
}
|
|
530
|
+
return cursor;
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
throw new dataTypes_1.DataError('"cursor" is not one this query returned; send the same table and sort with the cursor from the previous page.');
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
exports.decodeCursor = decodeCursor;
|
|
537
|
+
// Rows strictly after `values` in the order `keys`, as one condition. NULLs
|
|
538
|
+
// sort first ascending and last descending, as in SQLite. The last key is
|
|
539
|
+
// unique (the id), so no row is skipped or repeated across pages, even when
|
|
540
|
+
// rows share a sort value.
|
|
541
|
+
const keysetCondition = (keys, values, params) => {
|
|
542
|
+
const branches = [];
|
|
543
|
+
for (let i = 0; i < keys.length; i++) {
|
|
544
|
+
const parts = [];
|
|
545
|
+
for (let j = 0; j < i; j++) {
|
|
546
|
+
if (values[j] === null) {
|
|
547
|
+
parts.push(`${keys[j].sql} IS NULL`);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
parts.push(`${keys[j].sql} = ?`);
|
|
551
|
+
params.push(values[j]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const { sql, descending } = keys[i];
|
|
555
|
+
const value = values[i];
|
|
556
|
+
if (!descending) {
|
|
557
|
+
if (value === null) {
|
|
558
|
+
parts.push(`${sql} IS NOT NULL`);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
parts.push(`${sql} > ?`);
|
|
562
|
+
params.push(value);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
if (value === null)
|
|
567
|
+
continue;
|
|
568
|
+
parts.push(`(${sql} < ? OR ${sql} IS NULL)`);
|
|
569
|
+
params.push(value);
|
|
570
|
+
}
|
|
571
|
+
branches.push(`(${parts.join(' AND ')})`);
|
|
572
|
+
}
|
|
573
|
+
return branches.length ? `(${branches.join(' OR ')})` : '0';
|
|
574
|
+
};
|
|
575
|
+
exports.keysetCondition = keysetCondition;
|
|
576
|
+
// Inserts
|
|
577
|
+
// How many rows of `columnsPerRow` values fit in one statement.
|
|
578
|
+
const rowsPerInsert = (columnsPerRow) => Math.max(1, Math.floor(dataTypes_1.DATA_MAX_PARAMS / Math.max(1, columnsPerRow)));
|
|
579
|
+
exports.rowsPerInsert = rowsPerInsert;
|
|
580
|
+
// Splits rows into groups that each bind at most 100 parameters.
|
|
581
|
+
const chunkRows = (rows, columnsPerRow) => {
|
|
582
|
+
if (columnsPerRow > dataTypes_1.DATA_MAX_PARAMS) {
|
|
583
|
+
throw new dataTypes_1.DataError(`A row with ${columnsPerRow} values can't be written in one statement.`, 'too_large');
|
|
584
|
+
}
|
|
585
|
+
const size = (0, exports.rowsPerInsert)(columnsPerRow);
|
|
586
|
+
const out = [];
|
|
587
|
+
for (let i = 0; i < rows.length; i += size)
|
|
588
|
+
out.push(rows.slice(i, i + size));
|
|
589
|
+
return out;
|
|
590
|
+
};
|
|
591
|
+
exports.chunkRows = chunkRows;
|
|
592
|
+
// Arguments
|
|
593
|
+
// Models sometimes quote keys twice ("\"stock\"") or send "where" as a JSON
|
|
594
|
+
// string; both are read as meant. Keys inside a value a row stores (a json
|
|
595
|
+
// column) are left alone: only column names and our own operators are ours
|
|
596
|
+
// to tidy.
|
|
597
|
+
const tidyKey = (key) => key.replace(/^[\s"'`]+|[\s"'`]+$/g, '');
|
|
598
|
+
const tidy = (value) => {
|
|
599
|
+
if (Array.isArray(value))
|
|
600
|
+
return value.map(tidy);
|
|
601
|
+
if (isPlainObject(value)) {
|
|
602
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [tidyKey(k), tidy(v)]));
|
|
603
|
+
}
|
|
604
|
+
return value;
|
|
605
|
+
};
|
|
606
|
+
const VALUE_OPERATORS = ['increment', 'decrement', '$ref'];
|
|
607
|
+
// A value in "values", "rows" or "set": { "\"decrement\"": 2 } is ours,
|
|
608
|
+
// anything else is what the column stores, as sent.
|
|
609
|
+
const tidyValue = (value) => {
|
|
610
|
+
if (!isPlainObject(value))
|
|
611
|
+
return value;
|
|
612
|
+
const entries = Object.entries(value);
|
|
613
|
+
if (entries.length !== 1)
|
|
614
|
+
return value;
|
|
615
|
+
const key = tidyKey(entries[0][0]);
|
|
616
|
+
return VALUE_OPERATORS.includes(key) ? { [key]: entries[0][1] } : value;
|
|
617
|
+
};
|
|
618
|
+
const tidyColumns = (values) => isPlainObject(values)
|
|
619
|
+
? Object.fromEntries(Object.entries(values).map(([k, v]) => [tidyKey(k), tidyValue(v)]))
|
|
620
|
+
: values;
|
|
621
|
+
const tidyOperation = (op) => {
|
|
622
|
+
if (!isPlainObject(op))
|
|
623
|
+
return op;
|
|
624
|
+
const out = {};
|
|
625
|
+
for (const [rawKey, value] of Object.entries(op)) {
|
|
626
|
+
const key = tidyKey(rawKey);
|
|
627
|
+
out[key] =
|
|
628
|
+
key === 'values' || key === 'set'
|
|
629
|
+
? tidyColumns(value)
|
|
630
|
+
: key === 'rows' && Array.isArray(value)
|
|
631
|
+
? value.map(tidyColumns)
|
|
632
|
+
: key === 'where'
|
|
633
|
+
? tidy(value)
|
|
634
|
+
: value;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
};
|
|
638
|
+
const tidyArgs = (args) => {
|
|
639
|
+
if (!isPlainObject(args ?? {})) {
|
|
640
|
+
throw new dataTypes_1.DataError('Arguments are an object.');
|
|
641
|
+
}
|
|
642
|
+
const out = {};
|
|
643
|
+
for (const [rawKey, value] of Object.entries((args ?? {}))) {
|
|
644
|
+
out[tidyKey(rawKey)] = value;
|
|
645
|
+
}
|
|
646
|
+
for (const key of [
|
|
647
|
+
'where',
|
|
648
|
+
'aggregate',
|
|
649
|
+
'groupBy',
|
|
650
|
+
'sort',
|
|
651
|
+
'include',
|
|
652
|
+
'operations'
|
|
653
|
+
]) {
|
|
654
|
+
if (typeof out[key] === 'string') {
|
|
655
|
+
try {
|
|
656
|
+
out[key] = JSON.parse(out[key]);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// Left as is; the compiler explains the expected shape.
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
for (const [key, value] of Object.entries(out)) {
|
|
664
|
+
if (key === 'operations') {
|
|
665
|
+
out[key] = Array.isArray(value) ? value.map(tidyOperation) : value;
|
|
666
|
+
}
|
|
667
|
+
else if (key !== 'search' && key !== 'cursor') {
|
|
668
|
+
out[key] = tidy(value);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
672
|
+
};
|
|
673
|
+
exports.tidyArgs = tidyArgs;
|
|
674
|
+
const singleId = (value) => {
|
|
675
|
+
const n = typeof value === 'string' && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
676
|
+
return typeof n === 'number' && Number.isInteger(n);
|
|
677
|
+
};
|
|
678
|
+
// Whether an update can touch one row at most: it names an `id`, or its
|
|
679
|
+
// "where" pins id to one value ({ "id": 1 }, { "id": { "eq": 1 } },
|
|
680
|
+
// { "id": { "in": [1] } }), or it names a row inserted earlier in the same
|
|
681
|
+
// write ({ "$ref": 0 }, always a single-row insert). Other keys sit beside it
|
|
682
|
+
// with AND, so they only narrow it further.
|
|
683
|
+
const targetsOneRow = (op) => {
|
|
684
|
+
if (typeof op.id === 'number')
|
|
685
|
+
return true;
|
|
686
|
+
if (isPlainObject(op.id) && '$ref' in op.id)
|
|
687
|
+
return true;
|
|
688
|
+
if (!isPlainObject(op.where) || !('id' in op.where))
|
|
689
|
+
return false;
|
|
690
|
+
const id = op.where.id;
|
|
691
|
+
if (singleId(id))
|
|
692
|
+
return true;
|
|
693
|
+
if (!isPlainObject(id))
|
|
694
|
+
return false;
|
|
695
|
+
return (singleId(id.eq) ||
|
|
696
|
+
(Array.isArray(id.in) && id.in.length === 1 && singleId(id.in[0])));
|
|
697
|
+
};
|
|
698
|
+
const isRelativeChange = (value) => isPlainObject(value) &&
|
|
699
|
+
Object.keys(value).length === 1 &&
|
|
700
|
+
('increment' in value || 'decrement' in value);
|
|
701
|
+
// Whether a write is held for confirmation: it deletes, or sets a column to
|
|
702
|
+
// a value. Inserts are not, or every order would ask. A relative change (the
|
|
703
|
+
// stock an order uses) is not held when it reaches one row by id, or when the
|
|
704
|
+
// column has a minimum that stops it; otherwise "lower every price by
|
|
705
|
+
// 27,999" would run without asking. Without `schema`, only the id counts.
|
|
706
|
+
const isSensitiveWrite = (rawArgs, schema) => {
|
|
707
|
+
const args = (0, exports.tidyArgs)(rawArgs);
|
|
708
|
+
const operations = args.operations;
|
|
709
|
+
if (!Array.isArray(operations))
|
|
710
|
+
return true;
|
|
711
|
+
return operations.some(op => {
|
|
712
|
+
if (!isPlainObject(op))
|
|
713
|
+
return true;
|
|
714
|
+
if (op.op === 'insert')
|
|
715
|
+
return false;
|
|
716
|
+
if (op.op !== 'update')
|
|
717
|
+
return true;
|
|
718
|
+
const oneRow = targetsOneRow(op);
|
|
719
|
+
const table = schema?.tables.find(t => t.name === op.table);
|
|
720
|
+
return Object.entries(isPlainObject(op.set) ? op.set : {}).some(([name, value]) => {
|
|
721
|
+
if (!isRelativeChange(value))
|
|
722
|
+
return true;
|
|
723
|
+
if (oneRow)
|
|
724
|
+
return false;
|
|
725
|
+
const column = table?.columns.find(c => c.name === name);
|
|
726
|
+
return column?.min === null || column?.min === undefined;
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
exports.isSensitiveWrite = isSensitiveWrite;
|