Skip to content

Commit 53be59c

Browse files
authored
chore: add integration test for Todo model (#13)
* feat: post processing & integration tests * update tests * completing interated test for Todo * update zenstack version
1 parent 39f383c commit 53be59c

File tree

25 files changed

+4415
-142
lines changed

25 files changed

+4415
-142
lines changed

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"npm.packageManager": "pnpm",
3+
"eslint.packageManager": "pnpm"
4+
}

packages/internal/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/internal",
3-
"version": "0.1.18",
3+
"version": "0.1.20",
44
"description": "ZenStack internal runtime library",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

packages/internal/src/handler/data/handler.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
5555
break;
5656
}
5757
} catch (err: any) {
58-
console.error(`Error handling ${method} ${model}: ${err}`);
58+
console.log(`Error handling ${method} ${model}: ${err}`);
5959
if (err instanceof RequestHandlerError) {
6060
switch (err.code) {
6161
case ServerErrorCode.DENIED_BY_POLICY:
@@ -76,11 +76,18 @@ export default class DataHandler<DbClient> implements RequestHandler {
7676
message: err.message,
7777
});
7878
}
79-
} else if (err.code && PRISMA_ERROR_MAPPING[err.code]) {
80-
res.status(400).send({
81-
code: PRISMA_ERROR_MAPPING[err.code],
82-
message: 'database access error',
83-
});
79+
} else if (err.code) {
80+
if (PRISMA_ERROR_MAPPING[err.code]) {
81+
res.status(400).send({
82+
code: PRISMA_ERROR_MAPPING[err.code],
83+
message: 'database access error',
84+
});
85+
} else {
86+
res.status(400).send({
87+
code: 'PRISMA:' + err.code,
88+
message: 'an unhandled Prisma error occurred',
89+
});
90+
}
8491
} else {
8592
console.error(
8693
`An unknown error occurred: ${JSON.stringify(err)}`
@@ -110,7 +117,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
110117
if (id) {
111118
if (processedArgs.where) {
112119
processedArgs.where = {
113-
AND: [args.where, { id }],
120+
AND: [processedArgs.where, { id }],
114121
};
115122
} else {
116123
processedArgs.where = { id };
@@ -127,13 +134,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
127134
}
128135

129136
console.log(`Finding ${model}:\n${JSON.stringify(processedArgs)}`);
130-
await this.queryProcessor.postProcess(
131-
model,
132-
processedArgs,
133-
r,
134-
'read',
135-
context
136-
);
137+
await this.queryProcessor.postProcess(model, r, 'read', context);
137138

138139
res.status(200).send(r);
139140
}
@@ -190,13 +191,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
190191
return created;
191192
});
192193

193-
await this.queryProcessor.postProcess(
194-
model,
195-
processedArgs,
196-
r,
197-
'create',
198-
context
199-
);
194+
await this.queryProcessor.postProcess(model, r, 'create', context);
200195
res.status(201).send(r);
201196
}
202197

@@ -265,13 +260,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
265260
return updated;
266261
});
267262

268-
await this.queryProcessor.postProcess(
269-
model,
270-
updateArgs,
271-
r,
272-
'update',
273-
context
274-
);
263+
await this.queryProcessor.postProcess(model, r, 'update', context);
275264
res.status(200).send(r);
276265
}
277266

@@ -307,13 +296,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
307296
console.log(`Deleting ${model}:\n${JSON.stringify(delArgs)}`);
308297
const db = (this.service.db as any)[model];
309298
const r = await db.delete(delArgs);
310-
await this.queryProcessor.postProcess(
311-
model,
312-
delArgs,
313-
r,
314-
'delete',
315-
context
316-
);
299+
await this.queryProcessor.postProcess(model, r, 'delete', context);
317300

318301
res.status(200).send(r);
319302
}
@@ -334,7 +317,7 @@ export default class DataHandler<DbClient> implements RequestHandler {
334317
context
335318
);
336319
console.log(
337-
`Finding to-be-deleted ${model}:\n${JSON.stringify(readArgs)}`
320+
`Finding pre-operation ${model}:\n${JSON.stringify(readArgs)}`
338321
);
339322
const read = await db.findFirst(readArgs);
340323
if (!read) {

packages/internal/src/handler/data/query-processor.ts

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export class QueryProcessor {
3131
}
3232

3333
if (r.include || r.select) {
34+
if (r.include && r.select) {
35+
throw Error(
36+
'Passing both "include" and "select" at the same level of query is not supported'
37+
);
38+
}
39+
3440
// "include" and "select" are mutually exclusive
3541
const selector = r.include ? 'include' : 'select';
3642
for (const [field, value] of Object.entries(r[selector])) {
@@ -59,11 +65,129 @@ export class QueryProcessor {
5965
return r;
6066
}
6167

68+
private async getToOneFieldInfo(
69+
model: string,
70+
fieldName: string,
71+
fieldValue: any
72+
) {
73+
if (
74+
!!fieldValue &&
75+
!Array.isArray(fieldValue) &&
76+
typeof fieldValue === 'object' &&
77+
typeof fieldValue.id == 'string'
78+
) {
79+
return null;
80+
}
81+
82+
const fieldInfo = await this.service.resolveField(model, fieldName);
83+
if (!fieldInfo || fieldInfo.isArray) {
84+
return null;
85+
}
86+
87+
return fieldInfo;
88+
}
89+
90+
private async collectRelationFields(
91+
model: string,
92+
data: any,
93+
map: Map<string, string[]>
94+
) {
95+
for (const [fieldName, fieldValue] of Object.entries(data)) {
96+
const val: any = fieldValue;
97+
const fieldInfo = await this.getToOneFieldInfo(
98+
model,
99+
fieldName,
100+
fieldValue
101+
);
102+
if (!fieldInfo) {
103+
continue;
104+
}
105+
106+
if (!map.has(fieldInfo.type)) {
107+
map.set(fieldInfo.type, []);
108+
}
109+
map.get(fieldInfo.type)!.push(val.id);
110+
111+
// recurse into field value
112+
this.collectRelationFields(fieldInfo.type, val, map);
113+
}
114+
}
115+
116+
private async checkIdsAgainstPolicy(
117+
relationFieldMap: Map<string, string[]>,
118+
operation: PolicyOperationKind,
119+
context: QueryContext
120+
) {
121+
const promises = Array.from(relationFieldMap.entries()).map(
122+
async ([model, ids]) => {
123+
const args = {
124+
select: { id: true },
125+
where: {
126+
id: { in: ids },
127+
},
128+
};
129+
130+
const processedArgs = this.processQueryArgs(
131+
model,
132+
args,
133+
operation,
134+
context,
135+
true
136+
);
137+
138+
const checkedIds: Array<{ id: string }> = await this.service.db[
139+
model
140+
].findMany(processedArgs);
141+
return [model, checkedIds.map((r) => r.id)] as [
142+
string,
143+
string[]
144+
];
145+
}
146+
);
147+
return new Map<string, string[]>(await Promise.all(promises));
148+
}
149+
150+
private async sanitizeData(
151+
model: string,
152+
data: any,
153+
validatedIds: Map<string, string[]>
154+
) {
155+
for (const [fieldName, fieldValue] of Object.entries(data)) {
156+
const fieldInfo = await this.getToOneFieldInfo(
157+
model,
158+
fieldName,
159+
fieldValue
160+
);
161+
if (!fieldInfo) {
162+
continue;
163+
}
164+
const fv = fieldValue as { id: string };
165+
const valIds = validatedIds.get(fieldInfo.type);
166+
167+
if (!valIds || !valIds.includes(fv.id)) {
168+
console.log(
169+
`Deleting field ${fieldName} from ${model}#${data.id}, because field value #${fv.id} failed policy check`
170+
);
171+
delete data[fieldName];
172+
}
173+
174+
await this.sanitizeData(fieldInfo.type, fieldValue, validatedIds);
175+
}
176+
}
177+
62178
async postProcess(
63179
model: string,
64-
queryArgs: any,
65180
data: any,
66181
operation: PolicyOperationKind,
67182
context: QueryContext
68-
) {}
183+
) {
184+
const relationFieldMap = new Map<string, string[]>();
185+
await this.collectRelationFields(model, data, relationFieldMap);
186+
const validatedIds = await this.checkIdsAgainstPolicy(
187+
relationFieldMap,
188+
operation,
189+
context
190+
);
191+
await this.sanitizeData(model, data, validatedIds);
192+
}
69193
}

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "zenstack",
33
"displayName": "ZenStack CLI and Language Tools",
44
"description": "ZenStack CLI and Language Tools",
5-
"version": "0.1.38",
5+
"version": "0.1.40",
66
"engines": {
77
"vscode": "^1.56.0"
88
},

packages/schema/src/generator/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,17 @@ export class ZenStackGenerator {
3131
new PrismaGenerator(),
3232
new ServiceGenerator(),
3333
new ReactHooksGenerator(),
34-
new NextAuthGenerator(),
3534
];
3635

36+
try {
37+
require('next-auth');
38+
generators.push(new NextAuthGenerator());
39+
} catch {
40+
console.warn(
41+
'Next-auth module is not installed, skipping generating adapter.'
42+
);
43+
}
44+
3745
for (const generator of generators) {
3846
await generator.generate(context);
3947
}

packages/schema/src/generator/service/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Context, Generator } from '../types';
2-
import { Project, StructureKind } from 'ts-morph';
2+
import { Project, StructureKind, VariableDeclarationKind } from 'ts-morph';
33
import * as path from 'path';
44
import colors from 'colors';
55
import { INTERNAL_PACKAGE } from '../constants';
@@ -41,6 +41,15 @@ export default class ServiceGenerator implements Generator {
4141
.addBody()
4242
.setBodyText('return this._prisma;');
4343

44+
sf.addVariableStatement({
45+
declarationKind: VariableDeclarationKind.Let,
46+
declarations: [
47+
{
48+
name: 'guardModule',
49+
type: 'any',
50+
},
51+
],
52+
});
4453
cls
4554
.addMethod({
4655
name: 'resolveField',
@@ -57,8 +66,10 @@ export default class ServiceGenerator implements Generator {
5766
],
5867
})
5968
.addBody().setBodyText(`
60-
const module: any = await import('./query/guard');
61-
return module._fieldMapping?.[model]?.[field];
69+
if (!guardModule) {
70+
guardModule = await import('./query/guard');
71+
}
72+
return guardModule._fieldMapping?.[model]?.[field];
6273
`);
6374

6475
cls

packages/schema/src/language-server/zmodel-linker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,14 @@ export class ZModelLinker extends DefaultLinker {
176176
document: LangiumDocument<AstNode>,
177177
extraScopes: ScopeProvider[]
178178
) {
179-
this.resolve(node.left, document, extraScopes);
180-
this.resolve(node.right, document, extraScopes);
181179
switch (node.operator) {
182180
// TODO: support arithmetics?
183181
// case '+':
184182
// case '-':
185183
// case '*':
186184
// case '/':
185+
// this.resolve(node.left, document, extraScopes);
186+
// this.resolve(node.right, document, extraScopes);
187187
// this.resolveToBuiltinTypeOrDecl(node, 'Int');
188188
// break;
189189

@@ -195,6 +195,8 @@ export class ZModelLinker extends DefaultLinker {
195195
case '!=':
196196
case '&&':
197197
case '||':
198+
this.resolve(node.left, document, extraScopes);
199+
this.resolve(node.right, document, extraScopes);
198200
this.resolveToBuiltinTypeOrDecl(node, 'Boolean');
199201
break;
200202

0 commit comments

Comments
 (0)