Skip to content

Commit fb1ec3d

Browse files
authored
fix: issue with dealing with result read-back for nextjs and react hooks (#223)
2 parents d8afc2e + 6f1840b commit fb1ec3d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1520
-369
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ dist
55
.vscode/settings.json
66
.env.local
77
.npmcache
8+
coverage

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "1.0.0-alpha.45",
3+
"version": "1.0.0-alpha.48",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "1.0.0-alpha.45",
3+
"version": "1.0.0-alpha.48",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/next/jest.config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
export default {
7+
// Automatically clear mock calls, instances, contexts and results before every test
8+
clearMocks: true,
9+
10+
// Indicates whether the coverage information should be collected while executing the test
11+
collectCoverage: true,
12+
13+
// The directory where Jest should output its coverage files
14+
coverageDirectory: 'tests/coverage',
15+
16+
// An array of regexp pattern strings used to skip coverage collection
17+
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
18+
19+
// Indicates which provider should be used to instrument code for coverage
20+
coverageProvider: 'v8',
21+
22+
// A list of reporter names that Jest uses when writing coverage reports
23+
coverageReporters: ['json', 'text', 'lcov', 'clover'],
24+
25+
// A map from regular expressions to paths to transformers
26+
transform: { '^.+\\.tsx?$': 'ts-jest' },
27+
28+
testTimeout: 300000,
29+
};

packages/next/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/next",
3-
"version": "1.0.0-alpha.45",
3+
"version": "1.0.0-alpha.48",
44
"displayName": "ZenStack Next.js integration",
55
"description": "ZenStack Next.js integration",
66
"homepage": "https://zenstack.dev",
@@ -25,13 +25,20 @@
2525
},
2626
"dependencies": {
2727
"@zenstackhq/runtime": "workspace:*",
28-
"superjson": "^1.11.0"
28+
"@zenstackhq/testtools": "workspace:*",
29+
"tmp": "^0.2.1"
2930
},
3031
"devDependencies": {
32+
"@types/jest": "^29.4.0",
3133
"@types/react": "^18.0.26",
34+
"@types/supertest": "^2.0.12",
3235
"copyfiles": "^2.4.1",
36+
"jest": "^29.4.3",
3337
"react": "^17.0.2 || ^18",
3438
"rimraf": "^3.0.2",
39+
"superjson": "^1.11.0",
40+
"supertest": "^6.3.3",
41+
"ts-jest": "^29.0.5",
3542
"typescript": "^4.9.4"
3643
}
3744
}

packages/next/src/request-handler.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ async function handleRequest(
9494
return;
9595
}
9696
args = unmarshal(req.body, options.useSuperJson);
97+
// TODO: upsert's status code should be conditional
9798
resCode = 201;
9899
break;
99100

@@ -112,7 +113,7 @@ async function handleRequest(
112113

113114
case 'update':
114115
case 'updateMany':
115-
if (req.method !== 'PUT') {
116+
if (req.method !== 'PUT' && req.method !== 'PATCH') {
116117
res.status(400).send({ error: 'invalid http method' });
117118
return;
118119
}
@@ -130,10 +131,14 @@ async function handleRequest(
130131

131132
default:
132133
res.status(400).send({ error: `unknown method name: ${op}` });
133-
break;
134+
return;
134135
}
135136

136137
try {
138+
if (!prisma[model]) {
139+
res.status(400).send({ error: `unknown model name: ${model}` });
140+
return;
141+
}
137142
const result = await prisma[model][dbOp](args);
138143
res.status(resCode).send(marshal(result, options.useSuperJson));
139144
} catch (err) {
@@ -146,12 +151,14 @@ async function handleRequest(
146151
rejectedByPolicy: true,
147152
code: err.code,
148153
message: err.message,
154+
reason: err.meta?.reason,
149155
});
150156
} else {
151157
res.status(400).send({
152158
prisma: true,
153159
code: err.code,
154160
message: err.message,
161+
reason: err.meta?.reason,
155162
});
156163
}
157164
} else if (isPrismaClientUnknownRequestError(err) || isPrismaClientValidationError(err)) {
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { loadSchema } from '@zenstackhq/testtools';
3+
import { createServer, RequestListener } from 'http';
4+
import { apiResolver } from 'next/dist/server/api-utils/node';
5+
import superjson from 'superjson';
6+
import request from 'supertest';
7+
import { requestHandler, RequestHandlerOptions } from '../src/';
8+
9+
function makeTestClient(apiPath: string, options: RequestHandlerOptions, queryArgs?: unknown) {
10+
const pathParts = apiPath.split('/').filter((p) => p);
11+
12+
const query = {
13+
path: pathParts,
14+
...(queryArgs ? { q: superjson.stringify(queryArgs) } : {}),
15+
};
16+
17+
const handler = requestHandler(options);
18+
19+
const listener: RequestListener = (req, res) => {
20+
return apiResolver(
21+
req,
22+
res,
23+
query,
24+
handler,
25+
{
26+
previewModeEncryptionKey: '',
27+
previewModeId: '',
28+
previewModeSigningKey: '',
29+
},
30+
false
31+
);
32+
};
33+
34+
return request(createServer(listener));
35+
}
36+
37+
describe('request handler tests', () => {
38+
let origDir: string;
39+
40+
beforeEach(() => {
41+
origDir = process.cwd();
42+
});
43+
44+
afterEach(() => {
45+
process.chdir(origDir);
46+
});
47+
48+
it('simple crud', async () => {
49+
const model = `
50+
model M {
51+
id String @id @default(cuid())
52+
value Int
53+
}
54+
`;
55+
56+
const { prisma } = await loadSchema(model);
57+
58+
await makeTestClient('/m/create', { getPrisma: () => prisma })
59+
.post('/')
60+
.send({ data: { id: '1', value: 1 } })
61+
.expect(201)
62+
.expect((resp) => {
63+
expect(resp.body.json.value).toBe(1);
64+
});
65+
66+
await makeTestClient('/m/findUnique', { getPrisma: () => prisma }, { where: { id: '1' } })
67+
.get('/')
68+
.expect(200)
69+
.expect((resp) => {
70+
expect(resp.body.json.value).toBe(1);
71+
});
72+
73+
await makeTestClient('/m/findFirst', { getPrisma: () => prisma }, { where: { id: '1' } })
74+
.get('/')
75+
.expect(200)
76+
.expect((resp) => {
77+
expect(resp.body.json.value).toBe(1);
78+
});
79+
80+
await makeTestClient('/m/findMany', { getPrisma: () => prisma }, {})
81+
.get('/')
82+
.expect(200)
83+
.expect((resp) => {
84+
expect(resp.body.json).toHaveLength(1);
85+
});
86+
87+
await makeTestClient('/m/update', { getPrisma: () => prisma })
88+
.put('/')
89+
.send({ where: { id: '1' }, data: { value: 2 } })
90+
.expect(200)
91+
.expect((resp) => {
92+
expect(resp.body.json.value).toBe(2);
93+
});
94+
95+
await makeTestClient('/m/updateMany', { getPrisma: () => prisma })
96+
.put('/')
97+
.send({ data: { value: 4 } })
98+
.expect(200)
99+
.expect((resp) => {
100+
expect(resp.body.json.count).toBe(1);
101+
});
102+
103+
await makeTestClient('/m/upsert', { getPrisma: () => prisma })
104+
.post('/')
105+
.send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } })
106+
.expect(201)
107+
.expect((resp) => {
108+
expect(resp.body.json.value).toBe(2);
109+
});
110+
111+
await makeTestClient('/m/upsert', { getPrisma: () => prisma })
112+
.post('/')
113+
.send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } })
114+
.expect(201)
115+
.expect((resp) => {
116+
expect(resp.body.json.value).toBe(3);
117+
});
118+
119+
await makeTestClient('/m/count', { getPrisma: () => prisma }, { where: { id: '1' } })
120+
.get('/')
121+
.expect(200)
122+
.expect((resp) => {
123+
expect(resp.body.json).toBe(1);
124+
});
125+
126+
await makeTestClient('/m/count', { getPrisma: () => prisma }, {})
127+
.get('/')
128+
.expect(200)
129+
.expect((resp) => {
130+
expect(resp.body.json).toBe(2);
131+
});
132+
133+
await makeTestClient('/m/aggregate', { getPrisma: () => prisma }, { _sum: { value: true } })
134+
.get('/')
135+
.expect(200)
136+
.expect((resp) => {
137+
expect(resp.body.json._sum.value).toBe(7);
138+
});
139+
140+
await makeTestClient('/m/groupBy', { getPrisma: () => prisma }, { by: ['id'], _sum: { value: true } })
141+
.get('/')
142+
.expect(200)
143+
.expect((resp) => {
144+
const data = resp.body.json;
145+
expect(data).toHaveLength(2);
146+
expect(data.find((item: any) => item.id === '1')._sum.value).toBe(4);
147+
expect(data.find((item: any) => item.id === '2')._sum.value).toBe(3);
148+
});
149+
150+
await makeTestClient('/m/delete', { getPrisma: () => prisma }, { where: { id: '1' } })
151+
.del('/')
152+
.expect(200);
153+
expect(await prisma.m.count()).toBe(1);
154+
155+
await makeTestClient('/m/deleteMany', { getPrisma: () => prisma }, {})
156+
.del('/')
157+
.expect(200)
158+
.expect((resp) => {
159+
expect(resp.body.json.count).toBe(1);
160+
});
161+
expect(await prisma.m.count()).toBe(0);
162+
});
163+
164+
it('access policy crud', async () => {
165+
const model = `
166+
model M {
167+
id String @id @default(cuid())
168+
value Int
169+
170+
@@allow('create', true)
171+
@@allow('read', value > 0)
172+
@@allow('update', future().value > 1)
173+
@@allow('delete', value > 2)
174+
}
175+
`;
176+
177+
const { withPresets } = await loadSchema(model);
178+
179+
await makeTestClient('/m/create', { getPrisma: () => withPresets() })
180+
.post('/m/create')
181+
.send({ data: { value: 0 } })
182+
.expect(403)
183+
.expect((resp) => {
184+
expect(resp.body.reason).toBe('RESULT_NOT_READABLE');
185+
});
186+
187+
await makeTestClient('/m/create', { getPrisma: () => withPresets() })
188+
.post('/')
189+
.send({ data: { id: '1', value: 1 } })
190+
.expect(201);
191+
192+
await makeTestClient('/m/findMany', { getPrisma: () => withPresets() })
193+
.get('/')
194+
.expect(200)
195+
.expect((resp) => {
196+
expect(resp.body.json).toHaveLength(1);
197+
});
198+
199+
await makeTestClient('/m/update', { getPrisma: () => withPresets() })
200+
.put('/')
201+
.send({ where: { id: '1' }, data: { value: 0 } })
202+
.expect(403);
203+
204+
await makeTestClient('/m/update', { getPrisma: () => withPresets() })
205+
.put('/')
206+
.send({ where: { id: '1' }, data: { value: 2 } })
207+
.expect(200);
208+
209+
await makeTestClient('/m/delete', { getPrisma: () => withPresets() }, { where: { id: '1' } })
210+
.del('/')
211+
.expect(403);
212+
213+
await makeTestClient('/m/update', { getPrisma: () => withPresets() })
214+
.put('/')
215+
.send({ where: { id: '1' }, data: { value: 3 } })
216+
.expect(200);
217+
218+
await makeTestClient('/m/delete', { getPrisma: () => withPresets() }, { where: { id: '1' } })
219+
.del('/')
220+
.expect(200);
221+
});
222+
});

packages/plugins/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/react",
33
"displayName": "ZenStack plugin and runtime for ReactJS",
4-
"version": "1.0.0-alpha.45",
4+
"version": "1.0.0-alpha.48",
55
"description": "ZenStack plugin and runtime for ReactJS",
66
"main": "index.js",
77
"repository": {

packages/plugins/react/src/react-hooks-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function wrapReadbackErrorCheck(code: string) {
3535
return `try {
3636
${code}
3737
} catch (err: any) {
38-
if (err.info?.prisma && err.info?.code === 'P2004' && err.info?.extra === 'RESULT_NOT_READABLE') {
38+
if (err.info?.prisma && err.info?.code === 'P2004' && err.info?.reason === 'RESULT_NOT_READABLE') {
3939
// unable to readback data
4040
return undefined;
4141
} else {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "1.0.0-alpha.45",
4+
"version": "1.0.0-alpha.48",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

0 commit comments

Comments
 (0)