@smartive/graphql-magic 23.3.0 → 23.4.0-next.5
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/.github/workflows/release.yml +13 -2
- package/CHANGELOG.md +4 -2
- package/dist/bin/gqm.cjs +642 -63
- package/dist/cjs/index.cjs +2767 -2107
- package/dist/esm/migrations/generate-functions.d.ts +2 -0
- package/dist/esm/migrations/generate-functions.js +59 -0
- package/dist/esm/migrations/generate-functions.js.map +1 -0
- package/dist/esm/migrations/generate.d.ts +8 -1
- package/dist/esm/migrations/generate.js +273 -33
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/index.d.ts +2 -0
- package/dist/esm/migrations/index.js +2 -0
- package/dist/esm/migrations/index.js.map +1 -1
- package/dist/esm/migrations/parse-functions.d.ts +8 -0
- package/dist/esm/migrations/parse-functions.js +105 -0
- package/dist/esm/migrations/parse-functions.js.map +1 -0
- package/dist/esm/migrations/update-functions.d.ts +2 -0
- package/dist/esm/migrations/update-functions.js +174 -0
- package/dist/esm/migrations/update-functions.js.map +1 -0
- package/dist/esm/models/model-definitions.d.ts +4 -1
- package/dist/esm/resolvers/filters.js +73 -14
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/selects.js +33 -2
- package/dist/esm/resolvers/selects.js.map +1 -1
- package/dist/esm/resolvers/utils.d.ts +1 -0
- package/dist/esm/resolvers/utils.js +22 -0
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/docs/docs/3-fields.md +149 -0
- package/docs/docs/5-migrations.md +9 -1
- package/package.json +5 -1
- package/src/bin/gqm/gqm.ts +40 -5
- package/src/bin/gqm/settings.ts +7 -0
- package/src/migrations/generate-functions.ts +72 -0
- package/src/migrations/generate.ts +338 -41
- package/src/migrations/index.ts +2 -0
- package/src/migrations/parse-functions.ts +140 -0
- package/src/migrations/update-functions.ts +216 -0
- package/src/models/model-definitions.ts +4 -1
- package/src/resolvers/filters.ts +81 -25
- package/src/resolvers/selects.ts +38 -5
- package/src/resolvers/utils.ts +32 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export type ParsedFunction = {
|
|
4
|
+
name: string;
|
|
5
|
+
signature: string;
|
|
6
|
+
body: string;
|
|
7
|
+
fullDefinition: string;
|
|
8
|
+
isAggregate: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const normalizeFunctionBody = (body: string): string => {
|
|
12
|
+
return body
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.replace(/\s*\(\s*/g, '(')
|
|
15
|
+
.replace(/\s*\)\s*/g, ')')
|
|
16
|
+
.replace(/\s*,\s*/g, ',')
|
|
17
|
+
.trim();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const normalizeAggregateDefinition = (definition: string): string => {
|
|
21
|
+
let normalized = definition
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.replace(/\s*\(\s*/g, '(')
|
|
24
|
+
.replace(/\s*\)\s*/g, ')')
|
|
25
|
+
.replace(/\s*,\s*/g, ',')
|
|
26
|
+
.trim();
|
|
27
|
+
|
|
28
|
+
const initCondMatch = normalized.match(/INITCOND\s*=\s*([^,)]+)/i);
|
|
29
|
+
if (initCondMatch) {
|
|
30
|
+
const initCondValue = initCondMatch[1].trim();
|
|
31
|
+
const unquoted = initCondValue.replace(/^['"]|['"]$/g, '');
|
|
32
|
+
if (/^\d+$/.test(unquoted)) {
|
|
33
|
+
normalized = normalized.replace(/INITCOND\s*=\s*[^,)]+/i, `INITCOND = '${unquoted}'`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return normalized;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const extractFunctionSignature = (definition: string, isAggregate: boolean): string | null => {
|
|
41
|
+
if (isAggregate) {
|
|
42
|
+
const createMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+([^(]+)\(/i);
|
|
43
|
+
if (!createMatch) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const functionNamePart = createMatch[2].trim().replace(/^[^.]+\./, '');
|
|
48
|
+
const argsMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+[^(]+\(([^)]*)\)/i);
|
|
49
|
+
const args = argsMatch ? argsMatch[2].trim() : '';
|
|
50
|
+
|
|
51
|
+
return `${functionNamePart}(${args})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const createMatch = definition.match(/CREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+([^(]+)\(/i);
|
|
55
|
+
if (!createMatch) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const functionNamePart = createMatch[2].trim().replace(/^[^.]+\./, '');
|
|
60
|
+
const fullArgsMatch = definition.match(
|
|
61
|
+
/CREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+[^(]+\(([\s\S]*?)\)\s*(RETURNS|LANGUAGE|AS|STRICT|IMMUTABLE|STABLE|VOLATILE|SECURITY)/i,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!fullArgsMatch) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const argsSection = fullArgsMatch[2].trim();
|
|
69
|
+
const args = argsSection
|
|
70
|
+
.split(',')
|
|
71
|
+
.map((arg) => {
|
|
72
|
+
const trimmed = arg.trim();
|
|
73
|
+
const typeMatch = trimmed.match(/(\w+)\s+(\w+(?:\s*\[\])?)/);
|
|
74
|
+
if (typeMatch) {
|
|
75
|
+
return `${typeMatch[1]} ${typeMatch[2]}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return trimmed;
|
|
79
|
+
})
|
|
80
|
+
.join(', ');
|
|
81
|
+
|
|
82
|
+
return `${functionNamePart}(${args})`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const extractFunctionBody = (definition: string): string => {
|
|
86
|
+
const dollarQuoteMatch = definition.match(/AS\s+\$([^$]*)\$([\s\S]*?)\$\1\$/i);
|
|
87
|
+
if (dollarQuoteMatch) {
|
|
88
|
+
return dollarQuoteMatch[2].trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const bodyMatch = definition.match(/AS\s+\$\$([\s\S]*?)\$\$/i) || definition.match(/AS\s+['"]([\s\S]*?)['"]/i);
|
|
92
|
+
if (bodyMatch) {
|
|
93
|
+
return bodyMatch[1].trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return definition;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const parseFunctionsFile = (filePath: string): ParsedFunction[] => {
|
|
100
|
+
if (!existsSync(filePath)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
105
|
+
const functions: ParsedFunction[] = [];
|
|
106
|
+
|
|
107
|
+
const functionRegex =
|
|
108
|
+
/CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)\s+[\s\S]*?(?=CREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|AGGREGATE)|$)/gi;
|
|
109
|
+
let match;
|
|
110
|
+
let lastIndex = 0;
|
|
111
|
+
|
|
112
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
|
113
|
+
if (match.index < lastIndex) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
lastIndex = match.index + match[0].length;
|
|
117
|
+
|
|
118
|
+
const definition = match[0].trim();
|
|
119
|
+
const isAggregate = /CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE/i.test(definition);
|
|
120
|
+
const signature = extractFunctionSignature(definition, isAggregate);
|
|
121
|
+
|
|
122
|
+
if (!signature) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const nameMatch = signature.match(/^([^(]+)\(/);
|
|
127
|
+
const name = nameMatch ? nameMatch[1].trim().split('.').pop() || '' : '';
|
|
128
|
+
const body = isAggregate ? definition : extractFunctionBody(definition);
|
|
129
|
+
|
|
130
|
+
functions.push({
|
|
131
|
+
name,
|
|
132
|
+
signature,
|
|
133
|
+
body: isAggregate ? normalizeAggregateDefinition(body) : normalizeFunctionBody(body),
|
|
134
|
+
fullDefinition: definition,
|
|
135
|
+
isAggregate,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return functions;
|
|
140
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
import { ParsedFunction, parseFunctionsFile } from './parse-functions';
|
|
3
|
+
|
|
4
|
+
type DatabaseFunction = {
|
|
5
|
+
name: string;
|
|
6
|
+
signature: string;
|
|
7
|
+
body: string;
|
|
8
|
+
isAggregate: boolean;
|
|
9
|
+
definition?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const normalizeFunctionBody = (body: string): string => {
|
|
13
|
+
return body
|
|
14
|
+
.replace(/\s+/g, ' ')
|
|
15
|
+
.replace(/\s*\(\s*/g, '(')
|
|
16
|
+
.replace(/\s*\)\s*/g, ')')
|
|
17
|
+
.replace(/\s*,\s*/g, ',')
|
|
18
|
+
.trim();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const extractFunctionBody = (definition: string): string => {
|
|
22
|
+
const dollarQuoteMatch = definition.match(/AS\s+\$([^$]*)\$([\s\S]*?)\$\1\$/i);
|
|
23
|
+
if (dollarQuoteMatch) {
|
|
24
|
+
return dollarQuoteMatch[2].trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bodyMatch = definition.match(/AS\s+\$\$([\s\S]*?)\$\$/i) || definition.match(/AS\s+['"]([\s\S]*?)['"]/i);
|
|
28
|
+
if (bodyMatch) {
|
|
29
|
+
return bodyMatch[1].trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return definition;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getDatabaseFunctions = async (knex: Knex): Promise<DatabaseFunction[]> => {
|
|
36
|
+
const regularFunctions = await knex.raw(`
|
|
37
|
+
SELECT
|
|
38
|
+
p.proname as name,
|
|
39
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
40
|
+
pg_get_functiondef(p.oid) as definition,
|
|
41
|
+
false as is_aggregate
|
|
42
|
+
FROM pg_proc p
|
|
43
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
44
|
+
WHERE n.nspname = 'public'
|
|
45
|
+
AND NOT EXISTS (SELECT 1 FROM pg_aggregate a WHERE a.aggfnoid = p.oid)
|
|
46
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
const aggregateFunctions = await knex.raw(`
|
|
50
|
+
SELECT
|
|
51
|
+
p.proname as name,
|
|
52
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
53
|
+
a.aggtransfn::regproc::text as trans_func,
|
|
54
|
+
a.aggfinalfn::regproc::text as final_func,
|
|
55
|
+
a.agginitval as init_val,
|
|
56
|
+
pg_catalog.format_type(a.aggtranstype, NULL) as state_type,
|
|
57
|
+
true as is_aggregate
|
|
58
|
+
FROM pg_proc p
|
|
59
|
+
JOIN pg_aggregate a ON p.oid = a.aggfnoid
|
|
60
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
61
|
+
WHERE n.nspname = 'public'
|
|
62
|
+
ORDER BY p.proname, pg_get_function_identity_arguments(p.oid)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
const result: DatabaseFunction[] = [];
|
|
66
|
+
|
|
67
|
+
for (const row of regularFunctions.rows || []) {
|
|
68
|
+
const definition = row.definition || '';
|
|
69
|
+
const name = row.name || '';
|
|
70
|
+
const argumentsStr = row.arguments || '';
|
|
71
|
+
|
|
72
|
+
if (!definition) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const signature = `${name}(${argumentsStr})`;
|
|
77
|
+
const body = normalizeFunctionBody(extractFunctionBody(definition));
|
|
78
|
+
|
|
79
|
+
result.push({
|
|
80
|
+
name,
|
|
81
|
+
signature,
|
|
82
|
+
body,
|
|
83
|
+
isAggregate: false,
|
|
84
|
+
definition,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const row of aggregateFunctions.rows || []) {
|
|
89
|
+
const name = row.name || '';
|
|
90
|
+
const argumentsStr = row.arguments || '';
|
|
91
|
+
const transFunc = row.trans_func || '';
|
|
92
|
+
const finalFunc = row.final_func || '';
|
|
93
|
+
const initVal = row.init_val;
|
|
94
|
+
const stateType = row.state_type || '';
|
|
95
|
+
|
|
96
|
+
const signature = `${name}(${argumentsStr})`;
|
|
97
|
+
|
|
98
|
+
let aggregateDef = `CREATE AGGREGATE ${name}(${argumentsStr}) (`;
|
|
99
|
+
aggregateDef += `SFUNC = ${transFunc}, STYPE = ${stateType}`;
|
|
100
|
+
|
|
101
|
+
if (finalFunc) {
|
|
102
|
+
aggregateDef += `, FINALFUNC = ${finalFunc}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (initVal !== null && initVal !== undefined) {
|
|
106
|
+
const initValStr = typeof initVal === 'string' ? `'${initVal}'` : String(initVal);
|
|
107
|
+
aggregateDef += `, INITCOND = ${initValStr}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
aggregateDef += ');';
|
|
111
|
+
|
|
112
|
+
result.push({
|
|
113
|
+
name,
|
|
114
|
+
signature,
|
|
115
|
+
body: normalizeFunctionBody(aggregateDef),
|
|
116
|
+
isAggregate: true,
|
|
117
|
+
definition: aggregateDef,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const compareFunctions = (defined: ParsedFunction, db: DatabaseFunction): { changed: boolean; diff?: string } => {
|
|
125
|
+
const definedBody = normalizeFunctionBody(defined.body);
|
|
126
|
+
const dbBody = normalizeFunctionBody(db.body);
|
|
127
|
+
|
|
128
|
+
if (definedBody !== dbBody) {
|
|
129
|
+
const definedPreview = definedBody.length > 200 ? `${definedBody.substring(0, 200)}...` : definedBody;
|
|
130
|
+
const dbPreview = dbBody.length > 200 ? `${dbBody.substring(0, 200)}...` : dbBody;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
changed: true,
|
|
134
|
+
diff: `Definition changed:\n File: ${definedPreview}\n DB: ${dbPreview}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { changed: false };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const updateFunctions = async (knex: Knex, functionsFilePath: string): Promise<void> => {
|
|
142
|
+
const definedFunctions = parseFunctionsFile(functionsFilePath);
|
|
143
|
+
|
|
144
|
+
if (definedFunctions.length === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dbFunctions = await getDatabaseFunctions(knex);
|
|
149
|
+
const dbFunctionsBySignature = new Map<string, DatabaseFunction>();
|
|
150
|
+
for (const func of dbFunctions) {
|
|
151
|
+
dbFunctionsBySignature.set(func.signature, func);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.info(`Found ${definedFunctions.length} function(s) in file, ${dbFunctions.length} function(s) in database.`);
|
|
155
|
+
|
|
156
|
+
let updatedCount = 0;
|
|
157
|
+
let skippedCount = 0;
|
|
158
|
+
|
|
159
|
+
for (const definedFunc of definedFunctions) {
|
|
160
|
+
const dbFunc = dbFunctionsBySignature.get(definedFunc.signature);
|
|
161
|
+
|
|
162
|
+
if (!dbFunc) {
|
|
163
|
+
try {
|
|
164
|
+
await knex.raw(definedFunc.fullDefinition);
|
|
165
|
+
console.info(`✓ Created ${definedFunc.isAggregate ? 'aggregate' : 'function'}: ${definedFunc.signature}`);
|
|
166
|
+
updatedCount++;
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
console.error(
|
|
169
|
+
`✗ Failed to create ${definedFunc.isAggregate ? 'aggregate' : 'function'} ${definedFunc.signature}:`,
|
|
170
|
+
error.message,
|
|
171
|
+
);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
const comparison = compareFunctions(definedFunc, dbFunc);
|
|
176
|
+
if (comparison.changed) {
|
|
177
|
+
console.info(`\n⚠ ${definedFunc.isAggregate ? 'Aggregate' : 'Function'} ${definedFunc.signature} has changes:`);
|
|
178
|
+
if (comparison.diff) {
|
|
179
|
+
console.info(comparison.diff);
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
if (definedFunc.isAggregate) {
|
|
183
|
+
const dropMatch = definedFunc.fullDefinition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+([^(]+)\(/i);
|
|
184
|
+
if (dropMatch) {
|
|
185
|
+
const functionName = dropMatch[2].trim();
|
|
186
|
+
const argsMatch = definedFunc.fullDefinition.match(/CREATE\s+(OR\s+REPLACE\s+)?AGGREGATE\s+[^(]+\(([^)]*)\)/i);
|
|
187
|
+
const args = argsMatch ? argsMatch[2].trim() : '';
|
|
188
|
+
await knex.raw(`DROP AGGREGATE IF EXISTS ${functionName}${args ? `(${args})` : ''}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await knex.raw(definedFunc.fullDefinition);
|
|
192
|
+
console.info(`✓ Updated ${definedFunc.isAggregate ? 'aggregate' : 'function'}: ${definedFunc.signature}\n`);
|
|
193
|
+
updatedCount++;
|
|
194
|
+
} catch (error: any) {
|
|
195
|
+
console.error(
|
|
196
|
+
`✗ Failed to update ${definedFunc.isAggregate ? 'aggregate' : 'function'} ${definedFunc.signature}:`,
|
|
197
|
+
error.message,
|
|
198
|
+
);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.info(
|
|
203
|
+
`○ Skipped ${definedFunc.isAggregate ? 'aggregate' : 'function'} (unchanged): ${definedFunc.signature}`,
|
|
204
|
+
);
|
|
205
|
+
skippedCount++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.info(`\nSummary: ${updatedCount} updated, ${skippedCount} skipped`);
|
|
211
|
+
if (updatedCount > 0) {
|
|
212
|
+
console.info('Functions updated successfully.');
|
|
213
|
+
} else {
|
|
214
|
+
console.info('All functions are up to date.');
|
|
215
|
+
}
|
|
216
|
+
};
|
|
@@ -90,7 +90,10 @@ export type EntityFieldDefinition = FieldDefinitionBase &
|
|
|
90
90
|
indent?: boolean;
|
|
91
91
|
// If true the field is hidden in the admin interface
|
|
92
92
|
hidden?: boolean;
|
|
93
|
-
generateAs?:
|
|
93
|
+
generateAs?: {
|
|
94
|
+
expression: string;
|
|
95
|
+
type: 'virtual' | 'stored' | 'expression';
|
|
96
|
+
};
|
|
94
97
|
|
|
95
98
|
// Temporary fields for the generation of migrations
|
|
96
99
|
deleted?: true;
|
package/src/resolvers/filters.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { EntityModel, FullContext, getPermissionStack } from '..';
|
|
|
3
3
|
import { ForbiddenError, UserInputError } from '../errors';
|
|
4
4
|
import { OrderBy, Where, normalizeArguments } from './arguments';
|
|
5
5
|
import { FieldResolverNode } from './node';
|
|
6
|
-
import { Joins, QueryBuilderOps, addJoin, apply, applyJoins, getColumn, ors } from './utils';
|
|
6
|
+
import { Joins, QueryBuilderOps, addJoin, apply, applyJoins, getColumn, getColumnExpression, ors } from './utils';
|
|
7
7
|
|
|
8
8
|
export const SPECIAL_FILTERS: Record<string, string> = {
|
|
9
9
|
GT: '?? > ?',
|
|
@@ -157,19 +157,32 @@ const applyWhere = (node: FilterNode, where: Where | undefined, ops: QueryBuilde
|
|
|
157
157
|
// Should not happen
|
|
158
158
|
throw new Error(`Invalid filter ${key}.`);
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
const actualField = node.model.getField(actualKey);
|
|
161
|
+
const isExpressionField = actualField.generateAs?.type === 'expression';
|
|
162
|
+
const actualColumn = isExpressionField ? getColumnExpression(node, actualKey) : getColumn(node, actualKey);
|
|
163
|
+
if (isExpressionField) {
|
|
164
|
+
const operator = filter === 'GT' ? '>' : filter === 'GTE' ? '>=' : filter === 'LT' ? '<' : '<=';
|
|
165
|
+
ops.push((query) => query.whereRaw(`${actualColumn} ${operator} ?`, [value as string]));
|
|
166
|
+
} else {
|
|
167
|
+
ops.push((query) => query.whereRaw(SPECIAL_FILTERS[filter], [actualColumn, value as string]));
|
|
168
|
+
}
|
|
161
169
|
continue;
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
const field = node.model.getField(key);
|
|
165
173
|
|
|
166
|
-
const
|
|
174
|
+
const isExpressionField = field.generateAs?.type === 'expression';
|
|
175
|
+
const column = isExpressionField ? getColumnExpression(node, key) : getColumn(node, key);
|
|
167
176
|
|
|
168
177
|
if (field.kind === 'relation') {
|
|
169
178
|
const relation = node.model.getRelation(field.name);
|
|
170
179
|
|
|
171
180
|
if (value === null) {
|
|
172
|
-
|
|
181
|
+
if (isExpressionField) {
|
|
182
|
+
ops.push((query) => query.whereRaw(`${column} IS NULL`));
|
|
183
|
+
} else {
|
|
184
|
+
ops.push((query) => query.whereNull(column));
|
|
185
|
+
}
|
|
173
186
|
continue;
|
|
174
187
|
}
|
|
175
188
|
|
|
@@ -189,35 +202,65 @@ const applyWhere = (node: FilterNode, where: Where | undefined, ops: QueryBuilde
|
|
|
189
202
|
|
|
190
203
|
if (Array.isArray(value)) {
|
|
191
204
|
if (field && field.list) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
if (isExpressionField) {
|
|
206
|
+
ops.push((query) =>
|
|
207
|
+
ors(
|
|
208
|
+
query,
|
|
209
|
+
value.map((v) => (subQuery) => subQuery.whereRaw(`? = ANY(${column})`, [v])),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
ops.push((query) =>
|
|
214
|
+
ors(
|
|
215
|
+
query,
|
|
216
|
+
value.map((v) => (subQuery) => subQuery.whereRaw('? = ANY(??)', [v, column] as string[])),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
198
220
|
continue;
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
if (value.some((v) => v === null)) {
|
|
202
224
|
if (value.some((v) => v !== null)) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
if (isExpressionField) {
|
|
226
|
+
ops.push((query) =>
|
|
227
|
+
ors(query, [
|
|
228
|
+
(subQuery) => subQuery.whereRaw(`${column} IN (?)`, [value.filter((v) => v !== null) as string[]]),
|
|
229
|
+
(subQuery) => subQuery.whereRaw(`${column} IS NULL`),
|
|
230
|
+
]),
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
ops.push((query) =>
|
|
234
|
+
ors(query, [
|
|
235
|
+
(subQuery) => subQuery.whereIn(column, value.filter((v) => v !== null) as string[]),
|
|
236
|
+
(subQuery) => subQuery.whereNull(column),
|
|
237
|
+
]),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
209
240
|
continue;
|
|
210
241
|
}
|
|
211
242
|
|
|
212
|
-
|
|
243
|
+
if (isExpressionField) {
|
|
244
|
+
ops.push((query) => query.whereRaw(`${column} IS NULL`));
|
|
245
|
+
} else {
|
|
246
|
+
ops.push((query) => query.whereNull(column));
|
|
247
|
+
}
|
|
213
248
|
continue;
|
|
214
249
|
}
|
|
215
250
|
|
|
216
|
-
|
|
251
|
+
if (isExpressionField) {
|
|
252
|
+
ops.push((query) => query.whereRaw(`${column} IN (?)`, [value as string[]]));
|
|
253
|
+
} else {
|
|
254
|
+
ops.push((query) => query.whereIn(column, value as string[]));
|
|
255
|
+
}
|
|
217
256
|
continue;
|
|
218
257
|
}
|
|
219
258
|
|
|
220
|
-
|
|
259
|
+
if (isExpressionField) {
|
|
260
|
+
ops.push((query) => query.whereRaw(`${column} = ?`, [value]));
|
|
261
|
+
} else {
|
|
262
|
+
ops.push((query) => query.where({ [column]: value }));
|
|
263
|
+
}
|
|
221
264
|
}
|
|
222
265
|
};
|
|
223
266
|
|
|
@@ -226,11 +269,18 @@ const applySearch = (node: FieldResolverNode, search: string, query: Knex.QueryB
|
|
|
226
269
|
query,
|
|
227
270
|
node.model.fields
|
|
228
271
|
.filter(({ searchable }) => searchable)
|
|
229
|
-
.map(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
272
|
+
.map((field) => {
|
|
273
|
+
const isExpressionField = field.generateAs?.type === 'expression';
|
|
274
|
+
const column = isExpressionField ? getColumnExpression(node, field.name) : getColumn(node, field.name);
|
|
275
|
+
|
|
276
|
+
return (query: Knex.QueryBuilder) => {
|
|
277
|
+
if (isExpressionField) {
|
|
278
|
+
return query.whereRaw(`${column}::text ILIKE ?`, [`%${search}%`]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return query.whereRaw('??::text ILIKE ?', [column, `%${search}%`]);
|
|
282
|
+
};
|
|
283
|
+
}),
|
|
234
284
|
);
|
|
235
285
|
|
|
236
286
|
const applyOrderBy = (node: FilterNode, orderBy: OrderBy | OrderBy[], query: Knex.QueryBuilder, joins: Joins) => {
|
|
@@ -261,6 +311,12 @@ const applyOrderBy = (node: FilterNode, orderBy: OrderBy | OrderBy[], query: Kne
|
|
|
261
311
|
}
|
|
262
312
|
|
|
263
313
|
// Simple field
|
|
264
|
-
|
|
314
|
+
const isExpressionField = field.generateAs?.type === 'expression';
|
|
315
|
+
const column = isExpressionField ? getColumnExpression(node, key) : getColumn(node, key);
|
|
316
|
+
if (isExpressionField) {
|
|
317
|
+
void query.orderByRaw(`${column} ${value}`);
|
|
318
|
+
} else {
|
|
319
|
+
void query.orderBy(column, value as 'ASC' | 'DESC');
|
|
320
|
+
}
|
|
265
321
|
}
|
|
266
322
|
};
|
package/src/resolvers/selects.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isFieldNode,
|
|
14
14
|
} from '.';
|
|
15
15
|
import { PermissionError, UserInputError, getRole } from '..';
|
|
16
|
+
import { getColumnName } from './utils';
|
|
16
17
|
|
|
17
18
|
export const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
|
|
18
19
|
if (node.isAggregate) {
|
|
@@ -32,7 +33,15 @@ export const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins
|
|
|
32
33
|
...[
|
|
33
34
|
{ tableAlias: node.rootTableAlias, resultAlias: node.resultAlias, field: 'id', fieldAlias: ID_ALIAS },
|
|
34
35
|
...(node.model.root
|
|
35
|
-
? [
|
|
36
|
+
? [
|
|
37
|
+
{
|
|
38
|
+
tableAlias: node.rootTableAlias,
|
|
39
|
+
resultAlias: node.resultAlias,
|
|
40
|
+
field: 'type',
|
|
41
|
+
fieldAlias: TYPE_ALIAS,
|
|
42
|
+
generateAs: undefined,
|
|
43
|
+
},
|
|
44
|
+
]
|
|
36
45
|
: []),
|
|
37
46
|
...getSimpleFields(node)
|
|
38
47
|
.filter((fieldNode) => {
|
|
@@ -69,12 +78,36 @@ export const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins
|
|
|
69
78
|
tableAlias: field.inherited ? node.rootTableAlias : node.tableAlias,
|
|
70
79
|
resultAlias: node.resultAlias,
|
|
71
80
|
fieldAlias,
|
|
81
|
+
generateAs: field.generateAs,
|
|
72
82
|
};
|
|
73
83
|
}),
|
|
74
|
-
].map(
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
].map(({ tableAlias, resultAlias, field, fieldAlias, generateAs }) => {
|
|
85
|
+
if (generateAs?.type === 'expression') {
|
|
86
|
+
const tableShortAlias = node.ctx.aliases.getShort(tableAlias);
|
|
87
|
+
const resultShortAlias = node.ctx.aliases.getShort(resultAlias);
|
|
88
|
+
const expression = generateAs.expression.replace(/\b(\w+)\b/g, (match, columnName) => {
|
|
89
|
+
const field = node.model.fields.find((f) => {
|
|
90
|
+
if (f.name === columnName) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const actualColumnName = getColumnName(f);
|
|
94
|
+
|
|
95
|
+
return actualColumnName === columnName;
|
|
96
|
+
});
|
|
97
|
+
if (field) {
|
|
98
|
+
const actualColumnName = getColumnName(field);
|
|
99
|
+
|
|
100
|
+
return `${tableShortAlias}.${actualColumnName}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return match;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return node.ctx.knex.raw(`(${expression}) as ??`, [`${resultShortAlias}__${fieldAlias}`]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `${node.ctx.aliases.getShort(tableAlias)}.${field} as ${node.ctx.aliases.getShort(resultAlias)}__${fieldAlias}`;
|
|
110
|
+
}),
|
|
78
111
|
);
|
|
79
112
|
|
|
80
113
|
for (const subNode of getInlineFragments(node)) {
|
package/src/resolvers/utils.ts
CHANGED
|
@@ -218,6 +218,38 @@ export const getColumn = (
|
|
|
218
218
|
return `${node.ctx.aliases.getShort(field.inherited ? node.rootTableAlias : node.tableAlias)}.${getColumnName(field)}`;
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
+
export const getColumnExpression = (
|
|
222
|
+
node: Pick<ResolverNode, 'model' | 'ctx' | 'rootTableAlias' | 'tableAlias'>,
|
|
223
|
+
fieldName: string,
|
|
224
|
+
) => {
|
|
225
|
+
const field = node.model.fields.find((field) => field.name === fieldName)!;
|
|
226
|
+
|
|
227
|
+
if (field.generateAs?.type === 'expression') {
|
|
228
|
+
const expression = field.generateAs.expression.replace(/\b(\w+)\b/g, (match, columnName) => {
|
|
229
|
+
const referencedField = node.model.fields.find((f) => {
|
|
230
|
+
if (f.name === columnName) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
const actualColumnName = getColumnName(f);
|
|
234
|
+
|
|
235
|
+
return actualColumnName === columnName;
|
|
236
|
+
});
|
|
237
|
+
if (referencedField) {
|
|
238
|
+
const actualColumnName = getColumnName(referencedField);
|
|
239
|
+
const referencedTableAlias = referencedField.inherited ? node.rootTableAlias : node.tableAlias;
|
|
240
|
+
|
|
241
|
+
return `${node.ctx.aliases.getShort(referencedTableAlias)}.${actualColumnName}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return match;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return `(${expression})`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return getColumn(node, fieldName);
|
|
251
|
+
};
|
|
252
|
+
|
|
221
253
|
export const getTechnicalDisplay = (model: EntityModel, entity: Entity) =>
|
|
222
254
|
model.displayField && entity[model.displayField]
|
|
223
255
|
? `${model.name} "${entity[model.displayField]}" (${entity.id})`
|