rimecms 0.23.26 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-sqlite/where.js +222 -84
- package/package.json +1 -1
|
@@ -7,61 +7,40 @@ import {} from '../index.js';
|
|
|
7
7
|
import * as drizzleORM from 'drizzle-orm';
|
|
8
8
|
import { and, eq, getTableColumns, inArray, or } from 'drizzle-orm';
|
|
9
9
|
export const buildWhereParam = ({ query, slug, db, locale, tables, configCtx }) => {
|
|
10
|
+
// Helper to get table by key with correct typing
|
|
10
11
|
function getTable(key) {
|
|
11
12
|
return tables[key];
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function getTablesAndColumns(slug) {
|
|
15
|
+
// Get main table and localized table if applicable
|
|
16
|
+
const table = getTable(slug);
|
|
17
|
+
const tableNameLocales = withLocalesSuffix(slug);
|
|
18
|
+
const tableLocales = getTable(tableNameLocales);
|
|
19
|
+
// Get localized and unlocalized columns
|
|
20
|
+
const localizedColumns = locale && tableNameLocales in tables ? Object.keys(getTableColumns(tableLocales)) : [];
|
|
21
|
+
const unlocalizedColumns = Object.keys(getTableColumns(table));
|
|
22
|
+
return { table, tableLocales, localizedColumns, unlocalizedColumns };
|
|
23
|
+
}
|
|
24
|
+
const {
|
|
25
|
+
// Get main table and localized table if applicable
|
|
26
|
+
table, tableLocales, localizedColumns, unlocalizedColumns } = getTablesAndColumns(slug);
|
|
18
27
|
const buildCondition = (conditionObject) => {
|
|
19
28
|
// Handle nested AND conditions
|
|
20
29
|
if ('and' in conditionObject && Array.isArray(conditionObject.and)) {
|
|
21
|
-
const subConditions = conditionObject.and
|
|
22
|
-
.map((condition) => buildCondition(condition))
|
|
23
|
-
.filter(Boolean);
|
|
30
|
+
const subConditions = conditionObject.and.map((condition) => buildCondition(condition)).filter(Boolean);
|
|
24
31
|
return subConditions.length ? and(...subConditions) : false;
|
|
25
32
|
}
|
|
26
33
|
// Handle nested OR conditions
|
|
27
34
|
if ('or' in conditionObject && Array.isArray(conditionObject.or)) {
|
|
28
|
-
const subConditions = conditionObject.or
|
|
29
|
-
.map((condition) => buildCondition(condition))
|
|
30
|
-
.filter(Boolean);
|
|
35
|
+
const subConditions = conditionObject.or.map((condition) => buildCondition(condition)).filter(Boolean);
|
|
31
36
|
return subConditions.length ? or(...subConditions) : false;
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const idOperator = conditionObject.id;
|
|
38
|
-
delete conditionObject.id;
|
|
39
|
-
conditionObject.ownerId = idOperator;
|
|
40
|
-
}
|
|
41
|
-
// Handle versionId field for versioned collections
|
|
42
|
-
// if "versionId" inside the query it should refer to the id of the version table (current)
|
|
43
|
-
if (hasVersionsSuffix(slug) && 'versionId' in conditionObject) {
|
|
44
|
-
// Replace id with ownerId and keep the same operator and value
|
|
45
|
-
const idOperator = conditionObject.versionId;
|
|
46
|
-
delete conditionObject.versionId;
|
|
47
|
-
conditionObject.id = idOperator;
|
|
48
|
-
}
|
|
49
|
-
// Handle regular field conditions
|
|
50
|
-
const [column, operatorObj] = Object.entries(conditionObject)[0];
|
|
51
|
-
const [operator, rawValue] = Object.entries(operatorObj)[0];
|
|
52
|
-
if (!isOperator(operator)) {
|
|
53
|
-
throw new RimeError(RimeError.INVALID_DATA, operator + 'is not supported');
|
|
54
|
-
}
|
|
55
|
-
// get the correct Drizzle operator
|
|
56
|
-
const fn = operators[operator];
|
|
57
|
-
// Format compared value to support Date, Arrays,...
|
|
58
|
-
const value = formatValue({ operator, value: rawValue });
|
|
59
|
-
// Convert dot notation to double underscore
|
|
60
|
-
// for fields included in groups or tabs
|
|
61
|
-
const sqlColumn = column.replace(/\./g, '__');
|
|
38
|
+
conditionObject = normalizedForVersion(conditionObject, slug);
|
|
39
|
+
const {
|
|
40
|
+
// Get condition members
|
|
41
|
+
column, sqlColumn, fn, operator, rawValue, value } = getConditionMembers(conditionObject);
|
|
62
42
|
// Handle hierarchy fields (_parent, _position) in versioned collections
|
|
63
|
-
if (
|
|
64
|
-
(sqlColumn === '_parent' || sqlColumn === '_position' || sqlColumn === '_path')) {
|
|
43
|
+
if (shouldHandleVersionedHierarchyFields(slug, sqlColumn)) {
|
|
65
44
|
// Get the root table name by removing the '_versions' suffix
|
|
66
45
|
const rootSlug = slug.replace('_versions', '');
|
|
67
46
|
const rootTable = getTable(rootSlug);
|
|
@@ -82,64 +61,182 @@ export const buildWhereParam = ({ query, slug, db, locale, tables, configCtx })
|
|
|
82
61
|
// Look for a relation field
|
|
83
62
|
// Get document config
|
|
84
63
|
const documentConfig = configCtx.getBySlug(slug);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
64
|
+
// Get the compiled fields for lookups
|
|
65
|
+
const compiledFields = documentConfig.fields.map((f) => f.compile());
|
|
66
|
+
let fieldConfig = getFieldConfigByPath(column, compiledFields);
|
|
67
|
+
// Track relation-property detection (e.g. attributes.author.name)
|
|
68
|
+
let matchedPrefix = column;
|
|
69
|
+
let relationPropertyPath = null;
|
|
70
|
+
let isRelationProperty = false;
|
|
71
|
+
// If the exact path isn't found, try progressively shorter prefixes to support
|
|
72
|
+
// relation property queries like `attributes.author.name` -> `attributes.author`
|
|
90
73
|
if (!fieldConfig) {
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
const parts = column.split('.');
|
|
75
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
76
|
+
const prefix = parts.slice(0, i).join('.');
|
|
77
|
+
const candidate = getFieldConfigByPath(prefix, compiledFields);
|
|
78
|
+
if (candidate) {
|
|
79
|
+
fieldConfig = candidate;
|
|
80
|
+
// If prefix shorter than original column, record the property suffix
|
|
81
|
+
if (i < parts.length) {
|
|
82
|
+
matchedPrefix = prefix;
|
|
83
|
+
relationPropertyPath = parts.slice(i).join('.');
|
|
84
|
+
isRelationProperty = true;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// If still not found or not a relation field, log warning and return false condition
|
|
91
|
+
if (!fieldConfig || !isRelationField(fieldConfig)) {
|
|
92
|
+
const message = `the query contains the field "${column}", not found for ${documentConfig.slug} document`;
|
|
93
|
+
logger.warn(message);
|
|
93
94
|
// Return a condition that will always be false instead of returning false
|
|
94
95
|
// This ensures no documents match when a non-existent field is queried
|
|
95
|
-
return eq(table.id, '-1');
|
|
96
|
+
return eq(table.id, '-1');
|
|
97
|
+
}
|
|
98
|
+
// Helper: normalize various input forms (rawValue, CSV string, or formatted array)
|
|
99
|
+
// into a unique array of values used for multi-valued relation comparisons.
|
|
100
|
+
const normalizeValues = (raw, formatted) => {
|
|
101
|
+
let out;
|
|
102
|
+
if (Array.isArray(raw))
|
|
103
|
+
out = raw;
|
|
104
|
+
else if (typeof raw === 'string' && raw.includes(','))
|
|
105
|
+
out = raw.split(',');
|
|
106
|
+
else if (Array.isArray(formatted))
|
|
107
|
+
out = formatted;
|
|
108
|
+
else
|
|
109
|
+
out = [formatted];
|
|
110
|
+
return Array.from(new Set(out));
|
|
111
|
+
};
|
|
112
|
+
// Handle relation property queries (e.g., attributes.author.name)
|
|
113
|
+
// by building a subquery on the related collection
|
|
114
|
+
const buildRelationPropertyCondition = () => {
|
|
115
|
+
const relatedSlug = fieldConfig.relationTo;
|
|
116
|
+
const relatedTable = getTable(relatedSlug);
|
|
117
|
+
// Build a where clause for the related collection using the same operator/value
|
|
118
|
+
const relatedWhere = { [relationPropertyPath]: { [operator]: rawValue } };
|
|
119
|
+
// Recursive call to buildWhereParam for the related collection
|
|
120
|
+
const relatedCondition = buildWhereParam({
|
|
121
|
+
query: { where: relatedWhere },
|
|
122
|
+
slug: relatedSlug,
|
|
123
|
+
db,
|
|
124
|
+
locale,
|
|
125
|
+
tables,
|
|
126
|
+
configCtx
|
|
127
|
+
});
|
|
128
|
+
if (!relatedCondition) {
|
|
129
|
+
// No documents in related collection match => no parent documents match
|
|
130
|
+
return eq(table.id, '-1');
|
|
131
|
+
}
|
|
132
|
+
// Subquery of related document ids that match the property condition
|
|
133
|
+
const matchingRelatedIds = db.select({ id: relatedTable.id }).from(relatedTable).where(relatedCondition);
|
|
134
|
+
const relsTable = getTable(`${slug}Rels`);
|
|
135
|
+
// Join relation rows to documents by matching the related id and the relation path
|
|
136
|
+
const ownersWithMatching = db
|
|
137
|
+
.select({ id: relsTable.ownerId })
|
|
138
|
+
.from(relsTable)
|
|
139
|
+
.where(and(inArray(relsTable[`${relatedSlug}Id`], matchingRelatedIds), eq(relsTable.path, matchedPrefix), ...(fieldConfig.localized ? [eq(relsTable.locale, locale)] : [])));
|
|
140
|
+
return inArray(table.id, ownersWithMatching);
|
|
141
|
+
};
|
|
142
|
+
// If this is a relation property query, delegate to the relation property handler
|
|
143
|
+
if (isRelationProperty && relationPropertyPath) {
|
|
144
|
+
return buildRelationPropertyCondition();
|
|
96
145
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
// Handle direct relation field queries (e.g., attributes.author)
|
|
147
|
+
// only support a subset of operators for multi-valued relations
|
|
148
|
+
// Unsupported operator for multi-valued relations :
|
|
149
|
+
const supportedRelationManyOperators = ['equals', 'not_equals', 'in_array', 'not_in_array'];
|
|
150
|
+
// Only enforce the restriction for direct relation field queries.
|
|
151
|
+
// If this is a relation property query (e.g. attributes.author.name) we allow
|
|
152
|
+
// any operator because those will be applied to the related collection.
|
|
153
|
+
if (fieldConfig.many && !supportedRelationManyOperators.includes(operator)) {
|
|
154
|
+
const unsupportedMessage = `the operator "${operator}" is not supported for multi-valued relation field "${column}" in ${documentConfig.slug} document`;
|
|
155
|
+
logger.warn(unsupportedMessage);
|
|
100
156
|
// Return a condition that will always be false
|
|
101
|
-
return eq(table.id, '-1');
|
|
157
|
+
return eq(table.id, '-1');
|
|
102
158
|
}
|
|
103
|
-
//
|
|
104
|
-
// @TODO handle relation props ex: author.email
|
|
159
|
+
// Build relation condition
|
|
105
160
|
const [to, localized] = [fieldConfig.relationTo, fieldConfig.localized];
|
|
106
|
-
const
|
|
107
|
-
const
|
|
161
|
+
const relsTableName = `${slug}Rels`;
|
|
162
|
+
const relsTable = getTable(relsTableName);
|
|
163
|
+
// Helpers for building common relation-owner subqueries (operate on the relations table)
|
|
164
|
+
const buildOwnersWithTotalCount = (count) => db
|
|
165
|
+
.select({ id: relsTable.ownerId })
|
|
166
|
+
.from(relsTable)
|
|
167
|
+
.where(and(eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
168
|
+
.groupBy(relsTable.ownerId)
|
|
169
|
+
.having(drizzleORM.eq(drizzleORM.count(relsTable.id), count));
|
|
170
|
+
// Owners that have total relation count equal to the number of provided values
|
|
171
|
+
const buildOwnersWithMatchingCount = (values) => db
|
|
172
|
+
.select({ id: relsTable.ownerId })
|
|
173
|
+
.from(relsTable)
|
|
174
|
+
.where(and(drizzleORM.inArray(relsTable[`${to}Id`], values), eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
175
|
+
.groupBy(relsTable.ownerId)
|
|
176
|
+
.having(drizzleORM.eq(drizzleORM.count(relsTable.id), values.length));
|
|
177
|
+
// Owners that have at least one relation row NOT in the provided values
|
|
178
|
+
const buildOwnersWithNonMatching = (values) => db
|
|
179
|
+
.select({ id: relsTable.ownerId })
|
|
180
|
+
.from(relsTable)
|
|
181
|
+
.where(and(drizzleORM.notInArray(relsTable[`${to}Id`], values), eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
182
|
+
.groupBy(relsTable.ownerId)
|
|
183
|
+
.having(drizzleORM.gt(drizzleORM.count(relsTable.id), 0));
|
|
184
|
+
// Owners that have any relation rows (to exclude docs with no relations)
|
|
185
|
+
const buildOwnersWithRelations = () => db
|
|
186
|
+
.select({ id: relsTable.ownerId })
|
|
187
|
+
.from(relsTable)
|
|
188
|
+
.where(and(eq(relsTable.path, column), ...(localized ? [eq(relsTable.locale, locale)] : [])))
|
|
189
|
+
.groupBy(relsTable.ownerId)
|
|
190
|
+
.having(drizzleORM.gt(drizzleORM.count(relsTable.id), 0));
|
|
108
191
|
// Handle multi-valued relations specially when operator is `equals` to provide
|
|
109
192
|
// strict equality semantics (the relation set must equal the provided value(s)).
|
|
110
193
|
if (fieldConfig.many && operator === 'equals') {
|
|
111
|
-
// Accept
|
|
112
|
-
|
|
113
|
-
? rawValue
|
|
114
|
-
: typeof rawValue === 'string' && rawValue.includes(',')
|
|
115
|
-
? rawValue.split(',')
|
|
116
|
-
: [value];
|
|
117
|
-
// Ensure values are unique
|
|
118
|
-
values = Array.from(new Set(values));
|
|
194
|
+
// Accept various input forms and normalize to a unique array
|
|
195
|
+
const values = normalizeValues(rawValue, value);
|
|
119
196
|
// Owners with total relation count equal to values.length
|
|
120
|
-
const ownersWithTotalCount =
|
|
121
|
-
.select({ id: relationTable.ownerId })
|
|
122
|
-
.from(relationTable)
|
|
123
|
-
.where(and(...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
124
|
-
.groupBy(relationTable.ownerId)
|
|
125
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
197
|
+
const ownersWithTotalCount = buildOwnersWithTotalCount(values.length);
|
|
126
198
|
// Owners with matching relations count equal to values.length
|
|
127
|
-
const ownersWithMatchingCount =
|
|
128
|
-
.select({ id: relationTable.ownerId })
|
|
129
|
-
.from(relationTable)
|
|
130
|
-
.where(and(drizzleORM.inArray(relationTable[`${to}Id`], values), ...(localized ? [eq(relationTable.locale, locale)] : [])))
|
|
131
|
-
.groupBy(relationTable.ownerId)
|
|
132
|
-
.having(drizzleORM.eq(drizzleORM.count(relationTable.id), values.length));
|
|
199
|
+
const ownersWithMatchingCount = buildOwnersWithMatchingCount(values);
|
|
133
200
|
return and(inArray(table.id, ownersWithTotalCount), inArray(table.id, ownersWithMatchingCount));
|
|
134
201
|
}
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
202
|
+
// For multi-valued relations, allow `in_array` to act as a subset check:
|
|
203
|
+
// The provided values must contain ALL relation values of the document.
|
|
204
|
+
if (fieldConfig.many && operator === 'in_array') {
|
|
205
|
+
// Accept various input forms and normalize to a unique array
|
|
206
|
+
const values = normalizeValues(rawValue, value);
|
|
207
|
+
// Owners that have at least one relation row not included in the provided set
|
|
208
|
+
const ownersWithNonMatching = buildOwnersWithNonMatching(values);
|
|
209
|
+
// Owners that have any relation rows (to exclude docs with no relations)
|
|
210
|
+
const ownersWithRelations = buildOwnersWithRelations();
|
|
211
|
+
// Match documents that have relations and do NOT have any non-matching relation rows
|
|
212
|
+
return and(drizzleORM.notInArray(table.id, ownersWithNonMatching), inArray(table.id, ownersWithRelations));
|
|
213
|
+
}
|
|
214
|
+
// For multi-valued relations, `not_in_array` should match documents where the provided
|
|
215
|
+
// set does NOT contain all of the document's relation values (inverse of `in_array`).
|
|
216
|
+
if (fieldConfig.many && operator === 'not_in_array') {
|
|
217
|
+
// Accept various input forms and normalize to a unique array
|
|
218
|
+
const values = normalizeValues(rawValue, value);
|
|
219
|
+
const ownersWithNonMatching = buildOwnersWithNonMatching(values);
|
|
220
|
+
return inArray(table.id, ownersWithNonMatching);
|
|
139
221
|
}
|
|
222
|
+
// For multi-valued relations, `not_equals` is the inverse of `equals` (exact-set inequality)
|
|
223
|
+
if (fieldConfig.many && operator === 'not_equals') {
|
|
224
|
+
// Accept various input forms and normalize to a unique array
|
|
225
|
+
const values = normalizeValues(rawValue, value);
|
|
226
|
+
const ownersWithTotalCount = buildOwnersWithTotalCount(values.length);
|
|
227
|
+
const ownersWithMatchingCount = buildOwnersWithMatchingCount(values);
|
|
228
|
+
return or(drizzleORM.notInArray(table.id, ownersWithTotalCount), drizzleORM.notInArray(table.id, ownersWithMatchingCount));
|
|
229
|
+
}
|
|
230
|
+
// Handle single-valued relations and other operators
|
|
231
|
+
// Default behavior (membership checks with in_array etc.)
|
|
232
|
+
const conditions = [
|
|
233
|
+
eq(relsTable.path, column),
|
|
234
|
+
fn(relsTable[`${to}Id`], value),
|
|
235
|
+
...(localized ? [eq(relsTable.locale, locale)] : [])
|
|
236
|
+
];
|
|
140
237
|
return inArray(table.id, db
|
|
141
|
-
.select({ id:
|
|
142
|
-
.from(
|
|
238
|
+
.select({ id: relsTable.ownerId })
|
|
239
|
+
.from(relsTable)
|
|
143
240
|
.where(and(...conditions)));
|
|
144
241
|
};
|
|
145
242
|
return buildCondition(query.where);
|
|
@@ -162,6 +259,7 @@ const operators = {
|
|
|
162
259
|
greater_than: drizzleORM.gt
|
|
163
260
|
};
|
|
164
261
|
const isOperator = (str) => Object.keys(operators).includes(str);
|
|
262
|
+
// Format value based on operator and type
|
|
165
263
|
const formatValue = ({ operator, value }) => {
|
|
166
264
|
switch (true) {
|
|
167
265
|
case typeof value === 'string' &&
|
|
@@ -179,3 +277,43 @@ const formatValue = ({ operator, value }) => {
|
|
|
179
277
|
return value;
|
|
180
278
|
}
|
|
181
279
|
};
|
|
280
|
+
// Extract column, operator, and raw value from condition object
|
|
281
|
+
function getConditionMembers(obj) {
|
|
282
|
+
const [column, operatorObj] = Object.entries(obj)[0];
|
|
283
|
+
const [operator, rawValue] = Object.entries(operatorObj)[0];
|
|
284
|
+
if (!isOperator(operator)) {
|
|
285
|
+
throw new RimeError(RimeError.INVALID_DATA, operator + 'is not supported');
|
|
286
|
+
}
|
|
287
|
+
// get the correct Drizzle operator
|
|
288
|
+
const fn = operators[operator];
|
|
289
|
+
// Convert dot notation to double underscore
|
|
290
|
+
// for fields included in groups or tabs
|
|
291
|
+
const sqlColumn = column.replace(/\./g, '__');
|
|
292
|
+
// Format compared value to support Date, Arrays,...
|
|
293
|
+
const value = formatValue({ operator, value: rawValue });
|
|
294
|
+
return { column, sqlColumn, fn, operator, rawValue, value };
|
|
295
|
+
}
|
|
296
|
+
// Determine if we should handle versioned hierarchy fields
|
|
297
|
+
function shouldHandleVersionedHierarchyFields(slug, sqlColumn) {
|
|
298
|
+
return hasVersionsSuffix(slug) && (sqlColumn === '_parent' || sqlColumn === '_position' || sqlColumn === '_path');
|
|
299
|
+
}
|
|
300
|
+
// Normalize condition object for versioned collections
|
|
301
|
+
function normalizedForVersion(conditionObject, slug) {
|
|
302
|
+
// Handle id field for versioned collections
|
|
303
|
+
// if "id" inside the query it should refer to the root table
|
|
304
|
+
if (hasVersionsSuffix(slug) && 'id' in conditionObject) {
|
|
305
|
+
// Replace id with ownerId and keep the same operator and value
|
|
306
|
+
const idOperator = conditionObject.id;
|
|
307
|
+
delete conditionObject.id;
|
|
308
|
+
conditionObject.ownerId = idOperator;
|
|
309
|
+
}
|
|
310
|
+
// Handle versionId field for versioned collections
|
|
311
|
+
// if "versionId" inside the query it should refer to the id of the version table (current)
|
|
312
|
+
if (hasVersionsSuffix(slug) && 'versionId' in conditionObject) {
|
|
313
|
+
// Replace id with ownerId and keep the same operator and value
|
|
314
|
+
const idOperator = conditionObject.versionId;
|
|
315
|
+
delete conditionObject.versionId;
|
|
316
|
+
conditionObject.id = idOperator;
|
|
317
|
+
}
|
|
318
|
+
return conditionObject;
|
|
319
|
+
}
|