Skip to content

Commit

Permalink
fix: issue with dealing with result read-back for nextjs and react hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 committed Mar 2, 2023
1 parent d8afc2e commit 8e630e0
Show file tree
Hide file tree
Showing 49 changed files with 1,520 additions and 369 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist
.vscode/settings.json
.env.local
.npmcache
coverage
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"description": "",
"scripts": {
"build": "pnpm -r build",
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
29 changes: 29 additions & 0 deletions packages/next/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/

export default {
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,

// The directory where Jest should output its coverage files
coverageDirectory: 'tests/coverage',

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],

// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',

// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ['json', 'text', 'lcov', 'clover'],

// A map from regular expressions to paths to transformers
transform: { '^.+\\.tsx?$': 'ts-jest' },

testTimeout: 300000,
};
11 changes: 9 additions & 2 deletions packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/next",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"displayName": "ZenStack Next.js integration",
"description": "ZenStack Next.js integration",
"homepage": "https://zenstack.dev",
Expand All @@ -25,13 +25,20 @@
},
"dependencies": {
"@zenstackhq/runtime": "workspace:*",
"superjson": "^1.11.0"
"@zenstackhq/testtools": "workspace:*",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/jest": "^29.4.0",
"@types/react": "^18.0.26",
"@types/supertest": "^2.0.12",
"copyfiles": "^2.4.1",
"jest": "^29.4.3",
"react": "^17.0.2 || ^18",
"rimraf": "^3.0.2",
"superjson": "^1.11.0",
"supertest": "^6.3.3",
"ts-jest": "^29.0.5",
"typescript": "^4.9.4"
}
}
11 changes: 9 additions & 2 deletions packages/next/src/request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async function handleRequest(
return;
}
args = unmarshal(req.body, options.useSuperJson);
// TODO: upsert's status code should be conditional
resCode = 201;
break;

Expand All @@ -112,7 +113,7 @@ async function handleRequest(

case 'update':
case 'updateMany':
if (req.method !== 'PUT') {
if (req.method !== 'PUT' && req.method !== 'PATCH') {
res.status(400).send({ error: 'invalid http method' });
return;
}
Expand All @@ -130,10 +131,14 @@ async function handleRequest(

default:
res.status(400).send({ error: `unknown method name: ${op}` });
break;
return;
}

try {
if (!prisma[model]) {
res.status(400).send({ error: `unknown model name: ${model}` });
return;
}
const result = await prisma[model][dbOp](args);
res.status(resCode).send(marshal(result, options.useSuperJson));
} catch (err) {
Expand All @@ -146,12 +151,14 @@ async function handleRequest(
rejectedByPolicy: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
});
} else {
res.status(400).send({
prisma: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
});
}
} else if (isPrismaClientUnknownRequestError(err) || isPrismaClientValidationError(err)) {
Expand Down
222 changes: 222 additions & 0 deletions packages/next/tests/request-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { loadSchema } from '@zenstackhq/testtools';
import { createServer, RequestListener } from 'http';
import { apiResolver } from 'next/dist/server/api-utils/node';
import superjson from 'superjson';
import request from 'supertest';
import { requestHandler, RequestHandlerOptions } from '../src/';

function makeTestClient(apiPath: string, options: RequestHandlerOptions, queryArgs?: unknown) {
const pathParts = apiPath.split('/').filter((p) => p);

const query = {
path: pathParts,
...(queryArgs ? { q: superjson.stringify(queryArgs) } : {}),
};

const handler = requestHandler(options);

const listener: RequestListener = (req, res) => {
return apiResolver(
req,
res,
query,
handler,
{
previewModeEncryptionKey: '',
previewModeId: '',
previewModeSigningKey: '',
},
false
);
};

return request(createServer(listener));
}

describe('request handler tests', () => {
let origDir: string;

beforeEach(() => {
origDir = process.cwd();
});

afterEach(() => {
process.chdir(origDir);
});

it('simple crud', async () => {
const model = `
model M {
id String @id @default(cuid())
value Int
}
`;

const { prisma } = await loadSchema(model);

await makeTestClient('/m/create', { getPrisma: () => prisma })
.post('/')
.send({ data: { id: '1', value: 1 } })
.expect(201)
.expect((resp) => {
expect(resp.body.json.value).toBe(1);
});

await makeTestClient('/m/findUnique', { getPrisma: () => prisma }, { where: { id: '1' } })
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json.value).toBe(1);
});

await makeTestClient('/m/findFirst', { getPrisma: () => prisma }, { where: { id: '1' } })
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json.value).toBe(1);
});

await makeTestClient('/m/findMany', { getPrisma: () => prisma }, {})
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json).toHaveLength(1);
});

await makeTestClient('/m/update', { getPrisma: () => prisma })
.put('/')
.send({ where: { id: '1' }, data: { value: 2 } })
.expect(200)
.expect((resp) => {
expect(resp.body.json.value).toBe(2);
});

await makeTestClient('/m/updateMany', { getPrisma: () => prisma })
.put('/')
.send({ data: { value: 4 } })
.expect(200)
.expect((resp) => {
expect(resp.body.json.count).toBe(1);
});

await makeTestClient('/m/upsert', { getPrisma: () => prisma })
.post('/')
.send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } })
.expect(201)
.expect((resp) => {
expect(resp.body.json.value).toBe(2);
});

await makeTestClient('/m/upsert', { getPrisma: () => prisma })
.post('/')
.send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } })
.expect(201)
.expect((resp) => {
expect(resp.body.json.value).toBe(3);
});

await makeTestClient('/m/count', { getPrisma: () => prisma }, { where: { id: '1' } })
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json).toBe(1);
});

await makeTestClient('/m/count', { getPrisma: () => prisma }, {})
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json).toBe(2);
});

await makeTestClient('/m/aggregate', { getPrisma: () => prisma }, { _sum: { value: true } })
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json._sum.value).toBe(7);
});

await makeTestClient('/m/groupBy', { getPrisma: () => prisma }, { by: ['id'], _sum: { value: true } })
.get('/')
.expect(200)
.expect((resp) => {
const data = resp.body.json;
expect(data).toHaveLength(2);
expect(data.find((item: any) => item.id === '1')._sum.value).toBe(4);
expect(data.find((item: any) => item.id === '2')._sum.value).toBe(3);
});

await makeTestClient('/m/delete', { getPrisma: () => prisma }, { where: { id: '1' } })
.del('/')
.expect(200);
expect(await prisma.m.count()).toBe(1);

await makeTestClient('/m/deleteMany', { getPrisma: () => prisma }, {})
.del('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json.count).toBe(1);
});
expect(await prisma.m.count()).toBe(0);
});

it('access policy crud', async () => {
const model = `
model M {
id String @id @default(cuid())
value Int
@@allow('create', true)
@@allow('read', value > 0)
@@allow('update', future().value > 1)
@@allow('delete', value > 2)
}
`;

const { withPresets } = await loadSchema(model);

await makeTestClient('/m/create', { getPrisma: () => withPresets() })
.post('/m/create')
.send({ data: { value: 0 } })
.expect(403)
.expect((resp) => {
expect(resp.body.reason).toBe('RESULT_NOT_READABLE');
});

await makeTestClient('/m/create', { getPrisma: () => withPresets() })
.post('/')
.send({ data: { id: '1', value: 1 } })
.expect(201);

await makeTestClient('/m/findMany', { getPrisma: () => withPresets() })
.get('/')
.expect(200)
.expect((resp) => {
expect(resp.body.json).toHaveLength(1);
});

await makeTestClient('/m/update', { getPrisma: () => withPresets() })
.put('/')
.send({ where: { id: '1' }, data: { value: 0 } })
.expect(403);

await makeTestClient('/m/update', { getPrisma: () => withPresets() })
.put('/')
.send({ where: { id: '1' }, data: { value: 2 } })
.expect(200);

await makeTestClient('/m/delete', { getPrisma: () => withPresets() }, { where: { id: '1' } })
.del('/')
.expect(403);

await makeTestClient('/m/update', { getPrisma: () => withPresets() })
.put('/')
.send({ where: { id: '1' }, data: { value: 3 } })
.expect(200);

await makeTestClient('/m/delete', { getPrisma: () => withPresets() }, { where: { id: '1' } })
.del('/')
.expect(200);
});
});
2 changes: 1 addition & 1 deletion packages/plugins/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/react",
"displayName": "ZenStack plugin and runtime for ReactJS",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"description": "ZenStack plugin and runtime for ReactJS",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/react/src/react-hooks-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function wrapReadbackErrorCheck(code: string) {
return `try {
${code}
} catch (err: any) {
if (err.info?.prisma && err.info?.code === 'P2004' && err.info?.extra === 'RESULT_NOT_READABLE') {
if (err.info?.prisma && err.info?.code === 'P2004' && err.info?.reason === 'RESULT_NOT_READABLE') {
// unable to readback data
return undefined;
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/trpc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/trpc",
"displayName": "ZenStack plugin for tRPC",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
"version": "1.0.0-alpha.45",
"version": "1.0.0-alpha.48",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
const result = await this.utils.readWithCheck(this.model, readArgs);
if (result.length === 0) {
this.logger.warn(`${action} result cannot be read back`);
throw this.utils.deniedByPolicy(this.model, operation, 'RESULT_NOT_READABLE');
throw this.utils.deniedByPolicy(this.model, operation, 'result is not allowed to be read back');
} else if (result.length > 1) {
throw this.utils.unknownError('write unexpected resulted in multiple readback entities');
}
Expand Down
Loading

0 comments on commit 8e630e0

Please sign in to comment.