zenstack 0.1.41 → 1.0.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/.vscode/extensions.json +7 -0
- package/.vscode/launch.json +49 -0
- package/.vscode/settings.json +4 -0
- package/README.md +1 -0
- package/package.json +8 -90
- package/packages/internal/jest.config.ts +32 -0
- package/packages/internal/package.json +42 -0
- package/packages/internal/src/constants.ts +1 -0
- package/packages/internal/src/handler/data/guard-utils.ts +7 -0
- package/packages/internal/src/handler/data/handler.ts +415 -0
- package/packages/internal/src/handler/data/query-processor.ts +504 -0
- package/packages/internal/src/handler/index.ts +1 -0
- package/packages/internal/src/handler/types.ts +20 -0
- package/packages/internal/src/index.ts +3 -0
- package/packages/internal/src/request-handler.ts +27 -0
- package/packages/internal/src/request.ts +101 -0
- package/packages/internal/src/types.ts +40 -0
- package/packages/internal/tests/query-processor.test.ts +172 -0
- package/{out/cli/tsconfig.template.json → packages/internal/tsconfig.json} +7 -3
- package/packages/runtime/auth.d.ts +1 -0
- package/packages/runtime/auth.js +3 -0
- package/packages/runtime/hooks.d.ts +10 -0
- package/packages/runtime/hooks.js +3 -0
- package/packages/runtime/index.d.ts +3 -0
- package/packages/runtime/index.js +1 -0
- package/packages/runtime/package-lock.json +512 -0
- package/packages/runtime/package.json +16 -0
- package/packages/runtime/server.d.ts +1 -0
- package/packages/runtime/server.js +3 -0
- package/packages/runtime/types.d.ts +1 -0
- package/packages/runtime/types.js +3 -0
- package/packages/schema/.eslintrc.json +13 -0
- package/packages/schema/.vscodeignore +4 -0
- package/packages/schema/asset/logo-dark.png +0 -0
- package/packages/schema/asset/logo-light.png +0 -0
- package/{bin → packages/schema/bin}/cli +0 -0
- package/packages/schema/jest.config.ts +32 -0
- package/packages/schema/langium-config.json +14 -0
- package/packages/schema/langium-quickstart.md +41 -0
- package/packages/schema/language-configuration.json +30 -0
- package/packages/schema/package.json +96 -0
- package/packages/schema/src/cli/cli-util.ts +80 -0
- package/packages/schema/src/cli/index.ts +64 -0
- package/packages/schema/src/extension.ts +76 -0
- package/packages/schema/src/generator/constants.ts +5 -0
- package/packages/schema/src/generator/index.ts +92 -0
- package/{out/generator/next-auth/index.js → packages/schema/src/generator/next-auth/index.ts} +46 -58
- package/{out → packages/schema/src}/generator/package.template.json +0 -0
- package/packages/schema/src/generator/prisma/expression-writer.ts +352 -0
- package/packages/schema/src/generator/prisma/index.ts +32 -0
- package/packages/schema/src/generator/prisma/plain-expression-builder.ts +91 -0
- package/packages/schema/src/generator/prisma/prisma-builder.ts +366 -0
- package/packages/schema/src/generator/prisma/query-gard-generator.ts +208 -0
- package/packages/schema/src/generator/prisma/schema-generator.ts +300 -0
- package/packages/schema/src/generator/react-hooks/index.ts +181 -0
- package/packages/schema/src/generator/service/index.ts +107 -0
- package/{out → packages/schema/src}/generator/tsconfig.template.json +0 -0
- package/packages/schema/src/generator/types.ts +17 -0
- package/packages/schema/src/generator/utils.ts +9 -0
- package/packages/schema/src/language-server/generated/ast.ts +603 -0
- package/{out/language-server/generated/grammar.js → packages/schema/src/language-server/generated/grammar.ts} +5 -8
- package/packages/schema/src/language-server/generated/module.ts +24 -0
- package/packages/schema/src/language-server/main.ts +12 -0
- package/{out → packages/schema/src}/language-server/stdlib.zmodel +0 -0
- package/packages/schema/src/language-server/types.ts +9 -0
- package/packages/schema/src/language-server/zmodel-index.ts +33 -0
- package/packages/schema/src/language-server/zmodel-linker.ts +409 -0
- package/packages/schema/src/language-server/zmodel-module.ts +90 -0
- package/packages/schema/src/language-server/zmodel-scope.ts +21 -0
- package/packages/schema/src/language-server/zmodel-validator.ts +35 -0
- package/packages/schema/src/language-server/zmodel.langium +186 -0
- package/packages/schema/src/utils/exec-utils.ts +5 -0
- package/packages/schema/src/utils/indent-string.ts +6 -0
- package/packages/schema/syntaxes/zmodel.json +57 -0
- package/packages/schema/syntaxes/zmodel.tmLanguage.json +57 -0
- package/packages/schema/tests/generator/expression-writer.test.ts +676 -0
- package/packages/schema/tests/generator/prisma-builder.test.ts +138 -0
- package/packages/schema/tests/schema/parser.test.ts +423 -0
- package/packages/schema/tests/schema/sample-todo.test.ts +14 -0
- package/packages/schema/tests/utils.ts +38 -0
- package/packages/schema/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/samples/todo/.env +2 -0
- package/samples/todo/.eslintrc.json +3 -0
- package/samples/todo/.vscode/launch.json +11 -0
- package/samples/todo/README.md +34 -0
- package/samples/todo/components/AuthGuard.tsx +17 -0
- package/samples/todo/components/Avatar.tsx +22 -0
- package/samples/todo/components/BreadCrumb.tsx +44 -0
- package/samples/todo/components/ManageMembers.tsx +134 -0
- package/samples/todo/components/NavBar.tsx +57 -0
- package/samples/todo/components/SpaceMembers.tsx +76 -0
- package/samples/todo/components/Spaces.tsx +28 -0
- package/samples/todo/components/TimeInfo.tsx +17 -0
- package/samples/todo/components/Todo.tsx +72 -0
- package/samples/todo/components/TodoList.tsx +77 -0
- package/samples/todo/lib/context.ts +31 -0
- package/samples/todo/next.config.js +10 -0
- package/samples/todo/package-lock.json +7527 -0
- package/samples/todo/package.json +45 -0
- package/samples/todo/pages/_app.tsx +50 -0
- package/samples/todo/pages/api/auth/[...nextauth].ts +83 -0
- package/samples/todo/pages/api/zenstack/[...path].ts +16 -0
- package/samples/todo/pages/create-space.tsx +114 -0
- package/samples/todo/pages/index.tsx +32 -0
- package/samples/todo/pages/space/[slug]/[listId]/index.tsx +88 -0
- package/samples/todo/pages/space/[slug]/index.tsx +169 -0
- package/samples/todo/postcss.config.js +6 -0
- package/samples/todo/public/avatar.jpg +0 -0
- package/samples/todo/public/favicon.ico +0 -0
- package/samples/todo/public/logo.png +0 -0
- package/samples/todo/public/vercel.svg +4 -0
- package/samples/todo/styles/globals.css +7 -0
- package/samples/todo/tailwind.config.js +11 -0
- package/samples/todo/tsconfig.json +28 -0
- package/samples/todo/types/next-auth.d.ts +14 -0
- package/samples/todo/types/next.d.ts +16 -0
- package/samples/todo/zenstack/migrations/20221014084317_init/migration.sql +153 -0
- package/samples/todo/zenstack/migrations/20221020094651_upate_cli/migration.sql +23 -0
- package/samples/todo/zenstack/migrations/migration_lock.toml +3 -0
- package/samples/todo/zenstack/schema.prisma +126 -0
- package/samples/todo/zenstack/schema.zmodel +161 -0
- package/tests/integration/jest.config.ts +16 -0
- package/tests/integration/package-lock.json +1081 -0
- package/tests/integration/package.json +27 -0
- package/tests/integration/tests/operation-coverate.test.ts +563 -0
- package/tests/integration/tests/operations.zmodel +69 -0
- package/tests/integration/tests/todo-e2e.test.ts +577 -0
- package/tests/integration/tests/todo.zmodel +123 -0
- package/tests/integration/tests/tsconfig.template.json +10 -0
- package/tests/integration/tests/utils.ts +133 -0
- package/tests/integration/tsconfig.json +10 -0
- package/out/cli/cli-util.js +0 -64
- package/out/cli/cli-util.js.map +0 -1
- package/out/cli/generator.js +0 -1
- package/out/cli/generator.js.map +0 -1
- package/out/cli/index.js +0 -46
- package/out/cli/index.js.map +0 -1
- package/out/cli/package.template.json +0 -10
- package/out/extension.js +0 -81
- package/out/extension.js.map +0 -1
- package/out/generator/constants.js +0 -9
- package/out/generator/constants.js.map +0 -1
- package/out/generator/data-server/index.js +0 -1
- package/out/generator/data-server/index.js.map +0 -1
- package/out/generator/index.js +0 -98
- package/out/generator/index.js.map +0 -1
- package/out/generator/next-auth/index.js.map +0 -1
- package/out/generator/prisma/expression-writer.js +0 -287
- package/out/generator/prisma/expression-writer.js.map +0 -1
- package/out/generator/prisma/index.js +0 -38
- package/out/generator/prisma/index.js.map +0 -1
- package/out/generator/prisma/plain-expression-builder.js +0 -69
- package/out/generator/prisma/plain-expression-builder.js.map +0 -1
- package/out/generator/prisma/prisma-builder.js +0 -307
- package/out/generator/prisma/prisma-builder.js.map +0 -1
- package/out/generator/prisma/query-gard-generator.js +0 -159
- package/out/generator/prisma/query-gard-generator.js.map +0 -1
- package/out/generator/prisma/schema-generator.js +0 -201
- package/out/generator/prisma/schema-generator.js.map +0 -1
- package/out/generator/query-guard/index.js +0 -2
- package/out/generator/query-guard/index.js.map +0 -1
- package/out/generator/react-hooks/index.js +0 -179
- package/out/generator/react-hooks/index.js.map +0 -1
- package/out/generator/server/data/data-generator.js +0 -376
- package/out/generator/server/data/data-generator.js.map +0 -1
- package/out/generator/server/data/expression-writer.js +0 -287
- package/out/generator/server/data/expression-writer.js.map +0 -1
- package/out/generator/server/data/plain-expression-builder.js +0 -69
- package/out/generator/server/data/plain-expression-builder.js.map +0 -1
- package/out/generator/server/data-generator.js +0 -82
- package/out/generator/server/data-generator.js.map +0 -1
- package/out/generator/server/expression-writer.js +0 -1
- package/out/generator/server/expression-writer.js.map +0 -1
- package/out/generator/server/function/function-generator.js +0 -50
- package/out/generator/server/function/function-generator.js.map +0 -1
- package/out/generator/server/function-generator.js +0 -13
- package/out/generator/server/function-generator.js.map +0 -1
- package/out/generator/server/index.js +0 -88
- package/out/generator/server/index.js.map +0 -1
- package/out/generator/server/js-expression-builder.js +0 -1
- package/out/generator/server/js-expression-builder.js.map +0 -1
- package/out/generator/server/plain-expression-builder.js +0 -1
- package/out/generator/server/plain-expression-builder.js.map +0 -1
- package/out/generator/server/server-code-generator.js +0 -3
- package/out/generator/server/server-code-generator.js.map +0 -1
- package/out/generator/server/server-code-writer.js +0 -1
- package/out/generator/server/server-code-writer.js.map +0 -1
- package/out/generator/service/index.js +0 -133
- package/out/generator/service/index.js.map +0 -1
- package/out/generator/types.js +0 -10
- package/out/generator/types.js.map +0 -1
- package/out/generator/utils.js +0 -10
- package/out/generator/utils.js.map +0 -1
- package/out/language-server/generated/ast.js +0 -386
- package/out/language-server/generated/ast.js.map +0 -1
- package/out/language-server/generated/grammar.js.map +0 -1
- package/out/language-server/generated/module.js +0 -23
- package/out/language-server/generated/module.js.map +0 -1
- package/out/language-server/main.js +0 -12
- package/out/language-server/main.js.map +0 -1
- package/out/language-server/types.js +0 -3
- package/out/language-server/types.js.map +0 -1
- package/out/language-server/zmodel-index.js +0 -38
- package/out/language-server/zmodel-index.js.map +0 -1
- package/out/language-server/zmodel-linker.js +0 -241
- package/out/language-server/zmodel-linker.js.map +0 -1
- package/out/language-server/zmodel-module.js +0 -51
- package/out/language-server/zmodel-module.js.map +0 -1
- package/out/language-server/zmodel-scope.js +0 -30
- package/out/language-server/zmodel-scope.js.map +0 -1
- package/out/language-server/zmodel-validator.js +0 -25
- package/out/language-server/zmodel-validator.js.map +0 -1
- package/out/utils/indent-string.js +0 -9
- package/out/utils/indent-string.js.map +0 -1
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { makeClient, run, setup } from './utils';
|
|
3
|
+
|
|
4
|
+
describe('Todo E2E Tests', () => {
|
|
5
|
+
let workDir: string;
|
|
6
|
+
let origDir: string;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
origDir = path.resolve('.');
|
|
10
|
+
workDir = await setup('./tests/todo.zmodel');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
process.chdir(origDir);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
run('npx prisma migrate reset --schema ./zenstack/schema.prisma -f');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('user', async () => {
|
|
22
|
+
const userClient = makeClient('/api/data/User');
|
|
23
|
+
const user1 = {
|
|
24
|
+
id: 'user1',
|
|
25
|
+
email: 'user1@zenstack.dev',
|
|
26
|
+
name: 'User 1',
|
|
27
|
+
};
|
|
28
|
+
const user2 = {
|
|
29
|
+
id: 'user2',
|
|
30
|
+
email: 'user2@zenstack.dev',
|
|
31
|
+
name: 'User 2',
|
|
32
|
+
};
|
|
33
|
+
const user1Client = makeClient('/api/data/User/user1', user1.id);
|
|
34
|
+
const user2Client = makeClient('/api/data/User/user2', user1.id);
|
|
35
|
+
|
|
36
|
+
// create user1
|
|
37
|
+
await userClient
|
|
38
|
+
.post('/')
|
|
39
|
+
.send({
|
|
40
|
+
data: user1,
|
|
41
|
+
})
|
|
42
|
+
.expect(201)
|
|
43
|
+
.expect((resp) => {
|
|
44
|
+
expect(resp.body).toEqual(expect.objectContaining(user1));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const credClient = makeClient('/api/data/User', user1.id);
|
|
48
|
+
|
|
49
|
+
// find
|
|
50
|
+
await credClient
|
|
51
|
+
.get('/')
|
|
52
|
+
.expect(200)
|
|
53
|
+
.expect((resp) => {
|
|
54
|
+
expect(resp.body).toEqual([expect.objectContaining(user1)]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// get
|
|
58
|
+
await user1Client
|
|
59
|
+
.get('/')
|
|
60
|
+
.expect(200)
|
|
61
|
+
.expect((resp) =>
|
|
62
|
+
expect(resp.body).toEqual(expect.objectContaining(user1))
|
|
63
|
+
);
|
|
64
|
+
await user2Client.get('/').expect(404);
|
|
65
|
+
|
|
66
|
+
// create user2
|
|
67
|
+
await userClient.post('/').send({
|
|
68
|
+
data: user2,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// find with user1 should only get user1
|
|
72
|
+
await credClient
|
|
73
|
+
.get('/')
|
|
74
|
+
.expect(200)
|
|
75
|
+
.expect((resp) => {
|
|
76
|
+
expect(resp.body).toHaveLength(1);
|
|
77
|
+
expect(resp.body).toEqual([expect.objectContaining(user1)]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// get user2 as user1
|
|
81
|
+
await user2Client.get('/').expect(404);
|
|
82
|
+
|
|
83
|
+
// add both users into the same space
|
|
84
|
+
const spaceClient = makeClient('/api/data/Space', user1.id);
|
|
85
|
+
await spaceClient
|
|
86
|
+
.post('/')
|
|
87
|
+
.send({
|
|
88
|
+
data: {
|
|
89
|
+
name: 'Space 1',
|
|
90
|
+
slug: 'space1',
|
|
91
|
+
owner: { connect: { id: user1.id } },
|
|
92
|
+
members: {
|
|
93
|
+
create: [
|
|
94
|
+
{
|
|
95
|
+
user: { connect: { id: user1.id } },
|
|
96
|
+
role: 'ADMIN',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
user: { connect: { id: user2.id } },
|
|
100
|
+
role: 'USER',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
.expect(201);
|
|
107
|
+
|
|
108
|
+
// now both user1 and user2 should be visible
|
|
109
|
+
await credClient
|
|
110
|
+
.get('/')
|
|
111
|
+
.expect((resp) => expect(resp.body).toHaveLength(2));
|
|
112
|
+
await user2Client
|
|
113
|
+
.get('/')
|
|
114
|
+
.expect(200)
|
|
115
|
+
.expect((resp) =>
|
|
116
|
+
expect(resp.body).toEqual(expect.objectContaining(user2))
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// update user2 as user1
|
|
120
|
+
await user2Client
|
|
121
|
+
.put('/')
|
|
122
|
+
.send({
|
|
123
|
+
data: {
|
|
124
|
+
name: 'hello',
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
.expect(403);
|
|
128
|
+
|
|
129
|
+
// update user1 as user1
|
|
130
|
+
await user1Client
|
|
131
|
+
.put('/')
|
|
132
|
+
.send({
|
|
133
|
+
data: {
|
|
134
|
+
name: 'hello',
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
.expect(200)
|
|
138
|
+
.expect((resp) => expect(resp.body.name).toBe('hello'));
|
|
139
|
+
|
|
140
|
+
// delete user2 as user1
|
|
141
|
+
await user2Client.delete('/').expect(403);
|
|
142
|
+
|
|
143
|
+
// delete user1 as user1
|
|
144
|
+
await user1Client.delete('/').expect(200);
|
|
145
|
+
await user1Client.get('/').expect(404);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('todo list', async () => {
|
|
149
|
+
await createSpaceAndUsers();
|
|
150
|
+
|
|
151
|
+
const listClient = makeClient('/api/data/List');
|
|
152
|
+
await listClient
|
|
153
|
+
.post('/')
|
|
154
|
+
.send({
|
|
155
|
+
data: {
|
|
156
|
+
id: 'list1',
|
|
157
|
+
title: 'List 1',
|
|
158
|
+
owner: { connect: { id: user1.id } },
|
|
159
|
+
space: { connect: { id: space1.id } },
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
.expect(403);
|
|
163
|
+
|
|
164
|
+
const credClient = makeClient('/api/data/List', user1.id);
|
|
165
|
+
await credClient
|
|
166
|
+
.post('/')
|
|
167
|
+
.send({
|
|
168
|
+
data: {
|
|
169
|
+
id: 'list1',
|
|
170
|
+
title: 'List 1',
|
|
171
|
+
owner: { connect: { id: user1.id } },
|
|
172
|
+
space: { connect: { id: space1.id } },
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
.expect(201);
|
|
176
|
+
|
|
177
|
+
await credClient
|
|
178
|
+
.get('/')
|
|
179
|
+
.expect(200)
|
|
180
|
+
.expect((resp) => expect(resp.body).toHaveLength(1));
|
|
181
|
+
|
|
182
|
+
await listClient
|
|
183
|
+
.get('/')
|
|
184
|
+
.expect(200)
|
|
185
|
+
.expect((resp) => expect(resp.body).toHaveLength(0));
|
|
186
|
+
|
|
187
|
+
const list1Client = makeClient('/api/data/List/list1');
|
|
188
|
+
await list1Client.get('/').expect(404);
|
|
189
|
+
|
|
190
|
+
// accessible to owner
|
|
191
|
+
const list1CredClientUser1 = makeClient(
|
|
192
|
+
'/api/data/List/list1',
|
|
193
|
+
user1.id
|
|
194
|
+
);
|
|
195
|
+
await list1CredClientUser1
|
|
196
|
+
.get('/')
|
|
197
|
+
.expect(200)
|
|
198
|
+
.expect((resp) =>
|
|
199
|
+
expect(resp.body).toEqual(
|
|
200
|
+
expect.objectContaining({ id: 'list1', title: 'List 1' })
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// accessible to user in the space
|
|
205
|
+
const list1CredClientUser2 = makeClient(
|
|
206
|
+
'/api/data/List/list1',
|
|
207
|
+
user2.id
|
|
208
|
+
);
|
|
209
|
+
await list1CredClientUser2.get('/').expect(200);
|
|
210
|
+
|
|
211
|
+
// inaccessible to user not in the space
|
|
212
|
+
const list1CredClientUser3 = makeClient(
|
|
213
|
+
'/api/data/List/list1',
|
|
214
|
+
user3.id
|
|
215
|
+
);
|
|
216
|
+
await list1CredClientUser3.get('/').expect(404);
|
|
217
|
+
|
|
218
|
+
// make a private list
|
|
219
|
+
await credClient
|
|
220
|
+
.post('/')
|
|
221
|
+
.send({
|
|
222
|
+
data: {
|
|
223
|
+
id: 'list2',
|
|
224
|
+
title: 'List 2',
|
|
225
|
+
private: true,
|
|
226
|
+
owner: { connect: { id: user1.id } },
|
|
227
|
+
space: { connect: { id: space1.id } },
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
.expect(201);
|
|
231
|
+
|
|
232
|
+
// accessible to owner
|
|
233
|
+
const list2CredClientUser1 = makeClient(
|
|
234
|
+
'/api/data/List/list2',
|
|
235
|
+
user1.id
|
|
236
|
+
);
|
|
237
|
+
await list2CredClientUser1.get('/').expect(200);
|
|
238
|
+
|
|
239
|
+
// inaccessible to other user in the space
|
|
240
|
+
const list2CredClientUser2 = makeClient(
|
|
241
|
+
'/api/data/List/list2',
|
|
242
|
+
user2.id
|
|
243
|
+
);
|
|
244
|
+
await list2CredClientUser2.get('/').expect(404);
|
|
245
|
+
|
|
246
|
+
// create a list which doesn't match credential should fail
|
|
247
|
+
await credClient
|
|
248
|
+
.post('/')
|
|
249
|
+
.send({
|
|
250
|
+
data: {
|
|
251
|
+
id: 'list3',
|
|
252
|
+
title: 'List 3',
|
|
253
|
+
owner: { connect: { id: user2.id } },
|
|
254
|
+
space: { connect: { id: space1.id } },
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
.expect(403);
|
|
258
|
+
|
|
259
|
+
// create a list which doesn't match credential's space should fail
|
|
260
|
+
await credClient
|
|
261
|
+
.post('/')
|
|
262
|
+
.send({
|
|
263
|
+
data: {
|
|
264
|
+
id: 'list3',
|
|
265
|
+
title: 'List 3',
|
|
266
|
+
owner: { connect: { id: user1.id } },
|
|
267
|
+
space: { connect: { id: space2.id } },
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
.expect(403);
|
|
271
|
+
|
|
272
|
+
// update list
|
|
273
|
+
await list1CredClientUser1
|
|
274
|
+
.put('/')
|
|
275
|
+
.send({
|
|
276
|
+
data: {
|
|
277
|
+
title: 'List 1 updated',
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
.expect(200)
|
|
281
|
+
.expect((resp) => expect(resp.body.title).toBe('List 1 updated'));
|
|
282
|
+
|
|
283
|
+
await list1CredClientUser2
|
|
284
|
+
.put('/')
|
|
285
|
+
.send({
|
|
286
|
+
data: {
|
|
287
|
+
title: 'List 1 updated',
|
|
288
|
+
},
|
|
289
|
+
})
|
|
290
|
+
.expect(403);
|
|
291
|
+
|
|
292
|
+
// delete list
|
|
293
|
+
await list1CredClientUser2.delete('/').expect(403);
|
|
294
|
+
await list1CredClientUser1.delete('/').expect(200);
|
|
295
|
+
await list1CredClientUser1.get('/').expect(404);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('todo', async () => {
|
|
299
|
+
await createSpaceAndUsers();
|
|
300
|
+
|
|
301
|
+
const listClient = makeClient('/api/data/List', user1.id);
|
|
302
|
+
await listClient
|
|
303
|
+
.post('/')
|
|
304
|
+
.send({
|
|
305
|
+
data: {
|
|
306
|
+
id: 'list1',
|
|
307
|
+
title: 'List 1',
|
|
308
|
+
owner: { connect: { id: user1.id } },
|
|
309
|
+
space: { connect: { id: space1.id } },
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
.expect(201);
|
|
313
|
+
|
|
314
|
+
const todoClientUser1 = makeClient('/api/data/Todo', user1.id);
|
|
315
|
+
const todoClientUser2 = makeClient('/api/data/Todo', user2.id);
|
|
316
|
+
|
|
317
|
+
// create
|
|
318
|
+
await todoClientUser1
|
|
319
|
+
.post('/')
|
|
320
|
+
.send({
|
|
321
|
+
data: {
|
|
322
|
+
id: 'todo1',
|
|
323
|
+
title: 'Todo 1',
|
|
324
|
+
owner: { connect: { id: user1.id } },
|
|
325
|
+
list: {
|
|
326
|
+
connect: { id: 'list1' },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
.expect(201);
|
|
331
|
+
await todoClientUser2
|
|
332
|
+
.post('/')
|
|
333
|
+
.send({
|
|
334
|
+
data: {
|
|
335
|
+
id: 'todo2',
|
|
336
|
+
title: 'Todo 2',
|
|
337
|
+
owner: { connect: { id: user2.id } },
|
|
338
|
+
list: {
|
|
339
|
+
connect: { id: 'list1' },
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
.expect(201);
|
|
344
|
+
|
|
345
|
+
// read
|
|
346
|
+
await todoClientUser1
|
|
347
|
+
.get('/')
|
|
348
|
+
.expect(200)
|
|
349
|
+
.expect((resp) => expect(resp.body).toHaveLength(2));
|
|
350
|
+
await todoClientUser2
|
|
351
|
+
.get('/')
|
|
352
|
+
.expect(200)
|
|
353
|
+
.expect((resp) => expect(resp.body).toHaveLength(2));
|
|
354
|
+
|
|
355
|
+
const todo1ClientUser1 = makeClient('/api/data/Todo/todo1', user1.id);
|
|
356
|
+
const todo2ClientUser1 = makeClient('/api/data/Todo/todo2', user1.id);
|
|
357
|
+
|
|
358
|
+
// update, user in the same space can freely update
|
|
359
|
+
await todo1ClientUser1
|
|
360
|
+
.put('/')
|
|
361
|
+
.send({
|
|
362
|
+
data: {
|
|
363
|
+
title: 'Todo 1 updated',
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
.expect(200);
|
|
367
|
+
await todo2ClientUser1
|
|
368
|
+
.put('/')
|
|
369
|
+
.send({
|
|
370
|
+
data: {
|
|
371
|
+
title: 'Todo 2 updated',
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
.expect(200);
|
|
375
|
+
|
|
376
|
+
// create a private list
|
|
377
|
+
await listClient
|
|
378
|
+
.post('/')
|
|
379
|
+
.send({
|
|
380
|
+
data: {
|
|
381
|
+
id: 'list2',
|
|
382
|
+
private: true,
|
|
383
|
+
title: 'List 2',
|
|
384
|
+
owner: { connect: { id: user1.id } },
|
|
385
|
+
space: { connect: { id: space1.id } },
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
.expect(201);
|
|
389
|
+
|
|
390
|
+
// create
|
|
391
|
+
await todoClientUser1
|
|
392
|
+
.post('/')
|
|
393
|
+
.send({
|
|
394
|
+
data: {
|
|
395
|
+
id: 'todo3',
|
|
396
|
+
title: 'Todo 3',
|
|
397
|
+
owner: { connect: { id: user1.id } },
|
|
398
|
+
list: {
|
|
399
|
+
connect: { id: 'list2' },
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
})
|
|
403
|
+
.expect(201);
|
|
404
|
+
await todoClientUser2
|
|
405
|
+
.post('/')
|
|
406
|
+
.send({
|
|
407
|
+
data: {
|
|
408
|
+
id: 'todo4',
|
|
409
|
+
title: 'Todo 4',
|
|
410
|
+
owner: { connect: { id: user2.id } },
|
|
411
|
+
list: {
|
|
412
|
+
connect: { id: 'list2' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
.expect(403);
|
|
417
|
+
|
|
418
|
+
// update, only owner can update todo in a private list
|
|
419
|
+
const todo3ClientUser1 = makeClient('/api/data/Todo/todo3', user1.id);
|
|
420
|
+
const todo3ClientUser2 = makeClient('/api/data/Todo/todo3', user2.id);
|
|
421
|
+
await todo3ClientUser1
|
|
422
|
+
.put('/')
|
|
423
|
+
.send({
|
|
424
|
+
data: {
|
|
425
|
+
title: 'Todo 3 updated',
|
|
426
|
+
},
|
|
427
|
+
})
|
|
428
|
+
.expect(200);
|
|
429
|
+
await todo3ClientUser2
|
|
430
|
+
.put('/')
|
|
431
|
+
.send({
|
|
432
|
+
data: {
|
|
433
|
+
title: 'Todo 3 updated',
|
|
434
|
+
},
|
|
435
|
+
})
|
|
436
|
+
.expect(403);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('relation query', async () => {
|
|
440
|
+
await createSpaceAndUsers();
|
|
441
|
+
const listClient = makeClient('/api/data/List', user1.id);
|
|
442
|
+
await listClient.post('/').send({
|
|
443
|
+
data: {
|
|
444
|
+
id: 'list1',
|
|
445
|
+
title: 'List 1',
|
|
446
|
+
owner: { connect: { id: user1.id } },
|
|
447
|
+
space: { connect: { id: space1.id } },
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
await listClient.post('/').send({
|
|
451
|
+
data: {
|
|
452
|
+
id: 'list2',
|
|
453
|
+
title: 'List 2',
|
|
454
|
+
private: true,
|
|
455
|
+
owner: { connect: { id: user1.id } },
|
|
456
|
+
space: { connect: { id: space1.id } },
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const spaceClientUser1 = makeClient(
|
|
461
|
+
`/api/data/Space/space1`,
|
|
462
|
+
user1.id,
|
|
463
|
+
{
|
|
464
|
+
include: { lists: true },
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
await spaceClientUser1.get('/').expect((resp) => {
|
|
468
|
+
expect(resp.body.lists).toHaveLength(2);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const spaceClientUser2 = makeClient(
|
|
472
|
+
`/api/data/Space/space1`,
|
|
473
|
+
user2.id,
|
|
474
|
+
{
|
|
475
|
+
include: { lists: true },
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
await spaceClientUser2.get('/').expect((resp) => {
|
|
479
|
+
expect(resp.body.lists).toHaveLength(1);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const user1 = {
|
|
485
|
+
id: 'user1',
|
|
486
|
+
email: 'user1@zenstack.dev',
|
|
487
|
+
name: 'User 1',
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const user2 = {
|
|
491
|
+
id: 'user2',
|
|
492
|
+
email: 'user2@zenstack.dev',
|
|
493
|
+
name: 'User 2',
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const user3 = {
|
|
497
|
+
id: 'user3',
|
|
498
|
+
email: 'user3@zenstack.dev',
|
|
499
|
+
name: 'User 3',
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const space1 = {
|
|
503
|
+
id: 'space1',
|
|
504
|
+
name: 'Space 1',
|
|
505
|
+
slug: 'space1',
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const space2 = {
|
|
509
|
+
id: 'space2',
|
|
510
|
+
name: 'Space 2',
|
|
511
|
+
slug: 'space2',
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
async function createSpaceAndUsers() {
|
|
515
|
+
const userClient = makeClient('/api/data/User');
|
|
516
|
+
// create users
|
|
517
|
+
await userClient
|
|
518
|
+
.post('/')
|
|
519
|
+
.send({
|
|
520
|
+
data: user1,
|
|
521
|
+
})
|
|
522
|
+
.expect(201);
|
|
523
|
+
await userClient
|
|
524
|
+
.post('/')
|
|
525
|
+
.send({
|
|
526
|
+
data: user2,
|
|
527
|
+
})
|
|
528
|
+
.expect(201);
|
|
529
|
+
await userClient
|
|
530
|
+
.post('/')
|
|
531
|
+
.send({
|
|
532
|
+
data: user3,
|
|
533
|
+
})
|
|
534
|
+
.expect(201);
|
|
535
|
+
|
|
536
|
+
// add user1 and user2 into space1
|
|
537
|
+
const spaceClientUser1 = makeClient('/api/data/Space', user1.id);
|
|
538
|
+
await spaceClientUser1
|
|
539
|
+
.post('/')
|
|
540
|
+
.send({
|
|
541
|
+
data: {
|
|
542
|
+
...space1,
|
|
543
|
+
members: {
|
|
544
|
+
create: [
|
|
545
|
+
{
|
|
546
|
+
user: { connect: { id: user1.id } },
|
|
547
|
+
role: 'ADMIN',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
user: { connect: { id: user2.id } },
|
|
551
|
+
role: 'USER',
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
})
|
|
557
|
+
.expect(201);
|
|
558
|
+
|
|
559
|
+
// add user3 to space2
|
|
560
|
+
const spaceClientUser3 = makeClient('/api/data/Space', user3.id);
|
|
561
|
+
await spaceClientUser3
|
|
562
|
+
.post('/')
|
|
563
|
+
.send({
|
|
564
|
+
data: {
|
|
565
|
+
...space2,
|
|
566
|
+
members: {
|
|
567
|
+
create: [
|
|
568
|
+
{
|
|
569
|
+
user: { connect: { id: user3.id } },
|
|
570
|
+
role: 'ADMIN',
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
.expect(201);
|
|
577
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sample model for a collaborative Todo app
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = 'sqlite'
|
|
7
|
+
url = 'file:./todo.db'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model Space {
|
|
11
|
+
id String @id @default(uuid())
|
|
12
|
+
createdAt DateTime @default(now())
|
|
13
|
+
updatedAt DateTime @updatedAt
|
|
14
|
+
name String @length(1, 100)
|
|
15
|
+
slug String @unique @length(1, 20)
|
|
16
|
+
|
|
17
|
+
owner User? @relation(fields: [ownerId], references: [id])
|
|
18
|
+
ownerId String?
|
|
19
|
+
|
|
20
|
+
members SpaceUser[]
|
|
21
|
+
lists List[]
|
|
22
|
+
|
|
23
|
+
// require login
|
|
24
|
+
@@deny('all', auth() == null)
|
|
25
|
+
|
|
26
|
+
// everyone can create a space
|
|
27
|
+
@@allow('create', true)
|
|
28
|
+
|
|
29
|
+
// any user in the space can read the space
|
|
30
|
+
@@allow('read', members?[user == auth()])
|
|
31
|
+
|
|
32
|
+
// space admin can update and delete
|
|
33
|
+
@@allow('update,delete', members?[user == auth() && role == "ADMIN"])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
model SpaceUser {
|
|
37
|
+
id String @id @default(uuid())
|
|
38
|
+
createdAt DateTime @default(now())
|
|
39
|
+
updatedAt DateTime @updatedAt
|
|
40
|
+
space Space @relation(fields:[spaceId], references: [id], onDelete: Cascade)
|
|
41
|
+
spaceId String
|
|
42
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
43
|
+
userId String
|
|
44
|
+
role String
|
|
45
|
+
|
|
46
|
+
@@unique([userId, spaceId])
|
|
47
|
+
|
|
48
|
+
// require login
|
|
49
|
+
@@deny('all', auth() == null)
|
|
50
|
+
|
|
51
|
+
// space admin can create/update/delete
|
|
52
|
+
@@allow('create,update,delete', space.owner == auth() || space.members?[user == auth() && role == "ADMIN"])
|
|
53
|
+
|
|
54
|
+
// user can read entries for spaces which he's a member of
|
|
55
|
+
@@allow('read', space.members?[user == auth()])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
model User {
|
|
59
|
+
id String @id @default(uuid())
|
|
60
|
+
createdAt DateTime @default(now())
|
|
61
|
+
updatedAt DateTime @updatedAt
|
|
62
|
+
email String @unique @email
|
|
63
|
+
emailVerified DateTime?
|
|
64
|
+
password String?
|
|
65
|
+
name String? @length(1, 100)
|
|
66
|
+
|
|
67
|
+
ownedSpaces Space[]
|
|
68
|
+
|
|
69
|
+
spaces SpaceUser[]
|
|
70
|
+
image String? @url
|
|
71
|
+
lists List[]
|
|
72
|
+
todos Todo[]
|
|
73
|
+
|
|
74
|
+
// can be created by anyone, even not logged in
|
|
75
|
+
@@allow('create', true)
|
|
76
|
+
|
|
77
|
+
// can be read by users sharing any space
|
|
78
|
+
@@allow('read', auth() == this || spaces?[space.members?[user == auth()]])
|
|
79
|
+
|
|
80
|
+
// can only be updated and deleted by himeself
|
|
81
|
+
@@allow('update,delete', auth() == this)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
model List {
|
|
85
|
+
id String @id @default(uuid())
|
|
86
|
+
createdAt DateTime @default(now())
|
|
87
|
+
updatedAt DateTime @updatedAt
|
|
88
|
+
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
|
89
|
+
spaceId String
|
|
90
|
+
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
91
|
+
ownerId String
|
|
92
|
+
title String @length(1, 20)
|
|
93
|
+
private Boolean @default(false)
|
|
94
|
+
todos Todo[]
|
|
95
|
+
|
|
96
|
+
// require login
|
|
97
|
+
@@deny('all', auth() == null)
|
|
98
|
+
|
|
99
|
+
// can be read by owner or space members (only if not private)
|
|
100
|
+
@@allow('read', owner == auth() || (space.members?[user == auth()] && !private))
|
|
101
|
+
|
|
102
|
+
// can be updated/deleted by owner with a valid space
|
|
103
|
+
@@allow('create,update,delete', owner == auth() && space.members?[user == auth()])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
model Todo {
|
|
107
|
+
id String @id @default(uuid())
|
|
108
|
+
createdAt DateTime @default(now())
|
|
109
|
+
updatedAt DateTime @updatedAt
|
|
110
|
+
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
111
|
+
ownerId String
|
|
112
|
+
list List @relation(fields: [listId], references: [id], onDelete: Cascade)
|
|
113
|
+
listId String
|
|
114
|
+
title String
|
|
115
|
+
completedAt DateTime?
|
|
116
|
+
|
|
117
|
+
// require login
|
|
118
|
+
@@deny('all', auth() == null)
|
|
119
|
+
|
|
120
|
+
// owner has full access, also space members have full access (if the parent List is not private)
|
|
121
|
+
@@allow('all', list.owner == auth())
|
|
122
|
+
@@allow('all', list.space.members?[user == auth()] && !list.private)
|
|
123
|
+
}
|