zenstack 0.1.42 → 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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
|
3
|
+
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
|
4
|
+
|
|
5
|
+
// List of extensions which should be recommended for users of this workspace.
|
|
6
|
+
"recommendations": ["langium.langium-vscode"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A launch configuration that launches the extension inside a new window
|
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
|
3
|
+
// Hover to view descriptions of existing attributes.
|
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
+
{
|
|
6
|
+
"version": "0.2.0",
|
|
7
|
+
"configurations": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Generate for Todo Sample",
|
|
10
|
+
"program": "${workspaceFolder}/packages/schema/bin/cli",
|
|
11
|
+
"cwd": "${workspaceFolder}/samples/todo/",
|
|
12
|
+
"args": ["generate", "${workspaceFolder}/test.zmodel"],
|
|
13
|
+
"request": "launch",
|
|
14
|
+
"skipFiles": ["<node_internals>/**"],
|
|
15
|
+
"type": "node"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "Attach",
|
|
19
|
+
"port": 9229,
|
|
20
|
+
"request": "attach",
|
|
21
|
+
"skipFiles": ["<node_internals>/**"],
|
|
22
|
+
"type": "node"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "Sample-todo: debug server-side",
|
|
26
|
+
"type": "node-terminal",
|
|
27
|
+
"request": "launch",
|
|
28
|
+
"command": "npm run dev",
|
|
29
|
+
"cwd": "${workspaceFolder}/samples/todo"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Run Extension",
|
|
33
|
+
"type": "extensionHost",
|
|
34
|
+
"request": "launch",
|
|
35
|
+
"args": [
|
|
36
|
+
"--extensionDevelopmentPath=${workspaceFolder}/packages/schema"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "Attach to Language Server",
|
|
41
|
+
"type": "node",
|
|
42
|
+
"port": 6009,
|
|
43
|
+
"request": "attach",
|
|
44
|
+
"skipFiles": ["<node_internals>/**"],
|
|
45
|
+
"sourceMaps": true,
|
|
46
|
+
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>ZenStack</h1>
|
package/package.json
CHANGED
|
@@ -1,95 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zenstack",
|
|
3
|
-
"
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"categories": [
|
|
10
|
-
"Programming Languages"
|
|
11
|
-
],
|
|
12
|
-
"contributes": {
|
|
13
|
-
"languages": [
|
|
14
|
-
{
|
|
15
|
-
"id": "zmodel",
|
|
16
|
-
"aliases": [
|
|
17
|
-
"ZenStack Model",
|
|
18
|
-
"zmodel"
|
|
19
|
-
],
|
|
20
|
-
"extensions": [
|
|
21
|
-
".zmodel"
|
|
22
|
-
],
|
|
23
|
-
"configuration": "./language-configuration.json",
|
|
24
|
-
"icon": {
|
|
25
|
-
"light": "./asset/logo-light.png",
|
|
26
|
-
"dark": "./asset/logo-dark.png"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"grammars": [
|
|
31
|
-
{
|
|
32
|
-
"language": "zmodel",
|
|
33
|
-
"scopeName": "source.zmodel",
|
|
34
|
-
"path": "./syntaxes/zmodel.tmLanguage.json"
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
},
|
|
38
|
-
"activationEvents": [
|
|
39
|
-
"onLanguage:zmodel"
|
|
40
|
-
],
|
|
41
|
-
"files": [
|
|
42
|
-
"bin",
|
|
43
|
-
"out"
|
|
44
|
-
],
|
|
45
|
-
"bin": {
|
|
46
|
-
"zenstack": "./bin/cli"
|
|
47
|
-
},
|
|
48
|
-
"main": "./out/extension.js",
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"@zenstackhq/internal": "0.1.21",
|
|
51
|
-
"change-case": "^4.1.2",
|
|
52
|
-
"chevrotain": "^9.1.0",
|
|
53
|
-
"colors": "^1.4.0",
|
|
54
|
-
"commander": "^8.0.0",
|
|
55
|
-
"langium": "^0.4.0",
|
|
56
|
-
"prisma": "^4.4.0",
|
|
57
|
-
"promisify": "^0.0.3",
|
|
58
|
-
"ts-morph": "^16.0.0",
|
|
59
|
-
"vscode-jsonrpc": "^8.0.2",
|
|
60
|
-
"vscode-languageclient": "^7.0.0",
|
|
61
|
-
"vscode-languageserver": "^7.0.0",
|
|
62
|
-
"vscode-uri": "^3.0.2"
|
|
63
|
-
},
|
|
64
|
-
"devDependencies": {
|
|
65
|
-
"@prisma/internals": "^4.4.0",
|
|
66
|
-
"@types/jest": "^29.0.3",
|
|
67
|
-
"@types/node": "^14.18.29",
|
|
68
|
-
"@types/tmp": "^0.2.3",
|
|
69
|
-
"@types/uuid": "^8.3.4",
|
|
70
|
-
"@types/vscode": "^1.56.0",
|
|
71
|
-
"@typescript-eslint/eslint-plugin": "^4.14.1",
|
|
72
|
-
"@typescript-eslint/parser": "^4.14.1",
|
|
73
|
-
"concurrently": "^7.4.0",
|
|
74
|
-
"eslint": "^7.19.0",
|
|
75
|
-
"jest": "^29.0.3",
|
|
76
|
-
"langium-cli": "^0.4.0",
|
|
77
|
-
"tmp": "^0.2.1",
|
|
78
|
-
"ts-jest": "^29.0.1",
|
|
79
|
-
"ts-node": "^10.9.1",
|
|
80
|
-
"tsc-alias": "^1.7.0",
|
|
81
|
-
"tsconfig-paths-jest": "^0.0.1",
|
|
82
|
-
"typescript": "^4.6.2"
|
|
83
|
-
},
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"author": "",
|
|
8
|
+
"license": "ISC",
|
|
84
9
|
"scripts": {
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"ts:watch": "tsc --watch",
|
|
88
|
-
"tsc-alias:watch": "tsc-alias --watch",
|
|
89
|
-
"lint": "eslint src --ext ts",
|
|
90
|
-
"langium:generate": "langium generate",
|
|
91
|
-
"langium:watch": "langium generate --watch",
|
|
92
|
-
"watch": "concurrently --kill-others \"npm:langium:watch\" \"npm:ts:watch\" \"npm:tsc-alias:watch\"",
|
|
93
|
-
"test": "jest"
|
|
10
|
+
"build": "pnpm -r build",
|
|
11
|
+
"test": "pnpm -r test"
|
|
94
12
|
}
|
|
95
13
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* For a detailed explanation regarding each configuration property and type check, visit:
|
|
3
|
+
* https://jestjs.io/docs/configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import tsconfig from './tsconfig.json';
|
|
7
|
+
const moduleNameMapper = require('tsconfig-paths-jest')(tsconfig);
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
// Automatically clear mock calls, instances, contexts and results before every test
|
|
11
|
+
clearMocks: true,
|
|
12
|
+
|
|
13
|
+
// Indicates whether the coverage information should be collected while executing the test
|
|
14
|
+
collectCoverage: true,
|
|
15
|
+
|
|
16
|
+
// The directory where Jest should output its coverage files
|
|
17
|
+
coverageDirectory: 'tests/coverage',
|
|
18
|
+
|
|
19
|
+
// An array of regexp pattern strings used to skip coverage collection
|
|
20
|
+
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
|
|
21
|
+
|
|
22
|
+
// Indicates which provider should be used to instrument code for coverage
|
|
23
|
+
coverageProvider: 'v8',
|
|
24
|
+
|
|
25
|
+
// A list of reporter names that Jest uses when writing coverage reports
|
|
26
|
+
coverageReporters: ['json', 'text', 'lcov', 'clover'],
|
|
27
|
+
|
|
28
|
+
// A map from regular expressions to paths to transformers
|
|
29
|
+
transform: { '^.+\\.tsx?$': 'ts-jest' },
|
|
30
|
+
|
|
31
|
+
moduleNameMapper,
|
|
32
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zenstackhq/internal",
|
|
3
|
+
"version": "0.1.21",
|
|
4
|
+
"description": "ZenStack internal runtime library",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"watch": "tsc --watch",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"prepublishOnly": "pnpm build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"files": [
|
|
17
|
+
"lib/**/*"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"bcryptjs": "^2.4.3",
|
|
21
|
+
"deepcopy": "^2.1.0",
|
|
22
|
+
"swr": "^1.3.0",
|
|
23
|
+
"uuid": "^9.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"next": "12.3.1",
|
|
27
|
+
"react": "^17.0.2 || ^18",
|
|
28
|
+
"react-dom": "^17.0.2 || ^18"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bcryptjs": "^2.4.2",
|
|
32
|
+
"@types/jest": "^29.0.3",
|
|
33
|
+
"@types/node": "^14.18.29",
|
|
34
|
+
"@types/uuid": "^8.3.4",
|
|
35
|
+
"jest": "^29.0.3",
|
|
36
|
+
"ts-jest": "^29.0.1",
|
|
37
|
+
"ts-node": "^10.9.1",
|
|
38
|
+
"tsc-alias": "^1.7.0",
|
|
39
|
+
"tsconfig-paths-jest": "^0.0.1",
|
|
40
|
+
"typescript": "^4.6.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TRANSACTION_FIELD_NAME = 'zenstack_transaction';
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
import { RequestHandlerOptions } from '../../request-handler';
|
|
3
|
+
import {
|
|
4
|
+
PolicyOperationKind,
|
|
5
|
+
QueryContext,
|
|
6
|
+
ServerErrorCode,
|
|
7
|
+
Service,
|
|
8
|
+
} from '../../types';
|
|
9
|
+
import { RequestHandler, RequestHandlerError } from '../types';
|
|
10
|
+
import { QueryProcessor } from './query-processor';
|
|
11
|
+
import { v4 as uuid } from 'uuid';
|
|
12
|
+
import { TRANSACTION_FIELD_NAME } from '../../constants';
|
|
13
|
+
import { and } from './guard-utils';
|
|
14
|
+
|
|
15
|
+
const PRISMA_ERROR_MAPPING: Record<string, ServerErrorCode> = {
|
|
16
|
+
P2002: ServerErrorCode.UNIQUE_CONSTRAINT_VIOLATION,
|
|
17
|
+
P2003: ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION,
|
|
18
|
+
P2025: ServerErrorCode.REFERENCE_CONSTRAINT_VIOLATION,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default class DataHandler<DbClient> implements RequestHandler {
|
|
22
|
+
private readonly queryProcessor: QueryProcessor;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly service: Service<DbClient>,
|
|
26
|
+
private readonly options: RequestHandlerOptions
|
|
27
|
+
) {
|
|
28
|
+
this.queryProcessor = new QueryProcessor(service);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(req: NextApiRequest, res: NextApiResponse, path: string[]) {
|
|
32
|
+
const [model, id] = path;
|
|
33
|
+
const method = req.method;
|
|
34
|
+
|
|
35
|
+
const context = { user: await this.options.getServerUser(req, res) };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
switch (method) {
|
|
39
|
+
case 'GET':
|
|
40
|
+
await this.get(req, res, model, id, context);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'POST':
|
|
44
|
+
await this.post(req, res, model, context);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case 'PUT':
|
|
48
|
+
await this.put(req, res, model, id, context);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'DELETE':
|
|
52
|
+
await this.del(req, res, model, id, context);
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
console.warn(`Unhandled method: ${method}`);
|
|
57
|
+
res.status(200).send({});
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
console.log(`Error handling ${method} ${model}: ${err}`);
|
|
62
|
+
if (err instanceof RequestHandlerError) {
|
|
63
|
+
switch (err.code) {
|
|
64
|
+
case ServerErrorCode.DENIED_BY_POLICY:
|
|
65
|
+
res.status(403).send({
|
|
66
|
+
code: err.code,
|
|
67
|
+
message: err.message,
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
case ServerErrorCode.ENTITY_NOT_FOUND:
|
|
71
|
+
res.status(404).send({
|
|
72
|
+
code: err.code,
|
|
73
|
+
message: err.message,
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
res.status(400).send({
|
|
78
|
+
code: err.code,
|
|
79
|
+
message: err.message,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} else if (err.code) {
|
|
83
|
+
if (PRISMA_ERROR_MAPPING[err.code]) {
|
|
84
|
+
res.status(400).send({
|
|
85
|
+
code: PRISMA_ERROR_MAPPING[err.code],
|
|
86
|
+
message: 'database access error',
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
res.status(400).send({
|
|
90
|
+
code: 'PRISMA:' + err.code,
|
|
91
|
+
message: 'an unhandled Prisma error occurred',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
console.error(
|
|
96
|
+
`An unknown error occurred: ${JSON.stringify(err)}`
|
|
97
|
+
);
|
|
98
|
+
res.status(500).send({ error: ServerErrorCode.UNKNOWN });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async get(
|
|
104
|
+
req: NextApiRequest,
|
|
105
|
+
res: NextApiResponse,
|
|
106
|
+
model: string,
|
|
107
|
+
id: string,
|
|
108
|
+
context: QueryContext
|
|
109
|
+
) {
|
|
110
|
+
const db = (this.service.db as any)[model];
|
|
111
|
+
const args = req.query.q ? JSON.parse(req.query.q as string) : {};
|
|
112
|
+
const processedArgs = await this.queryProcessor.processQueryArgs(
|
|
113
|
+
model,
|
|
114
|
+
args,
|
|
115
|
+
'read',
|
|
116
|
+
context
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let r;
|
|
120
|
+
if (id) {
|
|
121
|
+
if (processedArgs.where) {
|
|
122
|
+
processedArgs.where = and(processedArgs.where, { id });
|
|
123
|
+
} else {
|
|
124
|
+
processedArgs.where = { id };
|
|
125
|
+
}
|
|
126
|
+
r = await db.findFirst(processedArgs);
|
|
127
|
+
if (!r) {
|
|
128
|
+
throw new RequestHandlerError(
|
|
129
|
+
ServerErrorCode.ENTITY_NOT_FOUND,
|
|
130
|
+
'not found'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
r = await db.findMany(processedArgs);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`Finding ${model}:\n${JSON.stringify(processedArgs)}`);
|
|
138
|
+
await this.queryProcessor.postProcess(model, r, 'read', context);
|
|
139
|
+
|
|
140
|
+
res.status(200).send(r);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async post(
|
|
144
|
+
req: NextApiRequest,
|
|
145
|
+
res: NextApiResponse,
|
|
146
|
+
model: string,
|
|
147
|
+
context: QueryContext
|
|
148
|
+
) {
|
|
149
|
+
const args = req.body;
|
|
150
|
+
if (!args) {
|
|
151
|
+
throw new RequestHandlerError(
|
|
152
|
+
ServerErrorCode.INVALID_REQUEST_PARAMS,
|
|
153
|
+
'body is required'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!args.data) {
|
|
157
|
+
throw new RequestHandlerError(
|
|
158
|
+
ServerErrorCode.INVALID_REQUEST_PARAMS,
|
|
159
|
+
'data field is required'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const db = this.service.db as any;
|
|
164
|
+
const transactionid = uuid();
|
|
165
|
+
const { writeArgs, includedModels } =
|
|
166
|
+
await this.queryProcessor.processQueryArgsForWrite(
|
|
167
|
+
model,
|
|
168
|
+
args,
|
|
169
|
+
'create',
|
|
170
|
+
context,
|
|
171
|
+
transactionid
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const r = await db.$transaction(async (tx: any) => {
|
|
175
|
+
console.log(`Create ${model}:\n${JSON.stringify(writeArgs)}`);
|
|
176
|
+
const created = await tx[model].create(writeArgs);
|
|
177
|
+
|
|
178
|
+
await this.checkPolicyForIncludedModels(
|
|
179
|
+
includedModels,
|
|
180
|
+
transactionid,
|
|
181
|
+
tx,
|
|
182
|
+
context
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const finalResultArgs = {
|
|
186
|
+
where: { id: created.id },
|
|
187
|
+
include: args.include,
|
|
188
|
+
select: args.select,
|
|
189
|
+
};
|
|
190
|
+
return await tx[model].findUnique(finalResultArgs);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await this.queryProcessor.postProcess(model, r, 'create', context);
|
|
194
|
+
res.status(201).send(r);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async checkPolicyForIncludedModels(
|
|
198
|
+
includedModels: Set<string>,
|
|
199
|
+
transactionId: string,
|
|
200
|
+
transaction: any,
|
|
201
|
+
context: QueryContext
|
|
202
|
+
) {
|
|
203
|
+
const modelChecks = Array.from(includedModels).map(
|
|
204
|
+
async (modelToCheck) => {
|
|
205
|
+
for (const operation of ['create', 'update', 'delete']) {
|
|
206
|
+
const queryArgs = {
|
|
207
|
+
where: {
|
|
208
|
+
[TRANSACTION_FIELD_NAME]: `${transactionId}:${operation}`,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const fullCount = await transaction[modelToCheck].count(
|
|
212
|
+
queryArgs
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (fullCount > 0) {
|
|
216
|
+
const processedQueryArgs =
|
|
217
|
+
await this.queryProcessor.processQueryArgs(
|
|
218
|
+
modelToCheck,
|
|
219
|
+
queryArgs,
|
|
220
|
+
operation as PolicyOperationKind,
|
|
221
|
+
context
|
|
222
|
+
);
|
|
223
|
+
console.log(
|
|
224
|
+
`Counting ${operation} ${modelToCheck}:\n${JSON.stringify(
|
|
225
|
+
processedQueryArgs
|
|
226
|
+
)}`
|
|
227
|
+
);
|
|
228
|
+
const filteredCount = await transaction[
|
|
229
|
+
modelToCheck
|
|
230
|
+
].count(processedQueryArgs);
|
|
231
|
+
|
|
232
|
+
if (fullCount !== filteredCount) {
|
|
233
|
+
console.log(
|
|
234
|
+
`Model ${modelToCheck}: filtered count ${filteredCount} mismatch full count ${fullCount}, transactionId: ${transactionId}`
|
|
235
|
+
);
|
|
236
|
+
throw new RequestHandlerError(
|
|
237
|
+
ServerErrorCode.DENIED_BY_POLICY,
|
|
238
|
+
'denied by policy'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (operation === 'delete' && fullCount > 0) {
|
|
244
|
+
// delete was converted to update during preprocessing, we need to proceed with it now
|
|
245
|
+
const deleteArgs = {
|
|
246
|
+
where: {
|
|
247
|
+
[TRANSACTION_FIELD_NAME]: `${transactionId}:delete`,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
console.log(
|
|
251
|
+
`Deleting nested entities for ${modelToCheck}:\n${JSON.stringify(
|
|
252
|
+
deleteArgs
|
|
253
|
+
)}`
|
|
254
|
+
);
|
|
255
|
+
await transaction[modelToCheck].deleteMany(deleteArgs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await Promise.all(modelChecks);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async put(
|
|
265
|
+
req: NextApiRequest,
|
|
266
|
+
res: NextApiResponse,
|
|
267
|
+
model: string,
|
|
268
|
+
id: string,
|
|
269
|
+
context: QueryContext
|
|
270
|
+
) {
|
|
271
|
+
if (!id) {
|
|
272
|
+
throw new RequestHandlerError(
|
|
273
|
+
ServerErrorCode.INVALID_REQUEST_PARAMS,
|
|
274
|
+
'missing "id" parameter'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ensure entity passes policy check
|
|
279
|
+
await this.ensureEntityPolicy(id, model, 'update', context);
|
|
280
|
+
|
|
281
|
+
const args = req.body;
|
|
282
|
+
if (!args) {
|
|
283
|
+
throw new RequestHandlerError(
|
|
284
|
+
ServerErrorCode.INVALID_REQUEST_PARAMS,
|
|
285
|
+
'body is required'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const db = this.service.db as any;
|
|
290
|
+
const transactionid = uuid();
|
|
291
|
+
args.where = { ...args.where, id };
|
|
292
|
+
|
|
293
|
+
const { preWriteGuard, writeArgs, includedModels } =
|
|
294
|
+
await this.queryProcessor.processQueryArgsForWrite(
|
|
295
|
+
model,
|
|
296
|
+
args,
|
|
297
|
+
'update',
|
|
298
|
+
context,
|
|
299
|
+
transactionid
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// make sure target matches policy before update
|
|
303
|
+
console.log(
|
|
304
|
+
`Finding pre-write record:\n${JSON.stringify(preWriteGuard)}`
|
|
305
|
+
);
|
|
306
|
+
let preUpdate = await db[model].findFirst(preWriteGuard);
|
|
307
|
+
if (preUpdate) {
|
|
308
|
+
// run post processing to see if any field is deleted, if so, reject
|
|
309
|
+
const deleted = await this.queryProcessor.postProcess(
|
|
310
|
+
model,
|
|
311
|
+
preUpdate,
|
|
312
|
+
'update',
|
|
313
|
+
context
|
|
314
|
+
);
|
|
315
|
+
if (deleted) {
|
|
316
|
+
preUpdate = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!preUpdate) {
|
|
321
|
+
console.log(`Pre-write guard check failed`);
|
|
322
|
+
throw new RequestHandlerError(
|
|
323
|
+
ServerErrorCode.DENIED_BY_POLICY,
|
|
324
|
+
'denied by policy before update'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const r = await db.$transaction(async (tx: any) => {
|
|
329
|
+
console.log(`Update ${model}:\n${JSON.stringify(writeArgs)}`);
|
|
330
|
+
await tx[model].update(writeArgs);
|
|
331
|
+
|
|
332
|
+
await this.checkPolicyForIncludedModels(
|
|
333
|
+
includedModels,
|
|
334
|
+
transactionid,
|
|
335
|
+
tx,
|
|
336
|
+
context
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const finalResultArgs = {
|
|
340
|
+
where: { id },
|
|
341
|
+
include: args.include,
|
|
342
|
+
select: args.select,
|
|
343
|
+
};
|
|
344
|
+
return await tx[model].findUnique(finalResultArgs);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await this.queryProcessor.postProcess(model, r, 'update', context);
|
|
348
|
+
res.status(200).send(r);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async del(
|
|
352
|
+
req: NextApiRequest,
|
|
353
|
+
res: NextApiResponse,
|
|
354
|
+
model: string,
|
|
355
|
+
id: string,
|
|
356
|
+
context: QueryContext
|
|
357
|
+
) {
|
|
358
|
+
if (!id) {
|
|
359
|
+
throw new RequestHandlerError(
|
|
360
|
+
ServerErrorCode.INVALID_REQUEST_PARAMS,
|
|
361
|
+
'missing "id" parameter'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ensure entity passes policy check
|
|
366
|
+
await this.ensureEntityPolicy(id, model, 'delete', context);
|
|
367
|
+
|
|
368
|
+
const args = req.query.q ? JSON.parse(req.query.q as string) : {};
|
|
369
|
+
|
|
370
|
+
// proceed with deleting
|
|
371
|
+
const delArgs = await this.queryProcessor.processQueryArgs(
|
|
372
|
+
model,
|
|
373
|
+
args,
|
|
374
|
+
'delete',
|
|
375
|
+
context,
|
|
376
|
+
false
|
|
377
|
+
);
|
|
378
|
+
delArgs.where = { ...delArgs.where, id };
|
|
379
|
+
|
|
380
|
+
console.log(`Deleting ${model}:\n${JSON.stringify(delArgs)}`);
|
|
381
|
+
const db = (this.service.db as any)[model];
|
|
382
|
+
const r = await db.delete(delArgs);
|
|
383
|
+
await this.queryProcessor.postProcess(model, r, 'delete', context);
|
|
384
|
+
|
|
385
|
+
res.status(200).send(r);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async ensureEntityPolicy(
|
|
389
|
+
id: string,
|
|
390
|
+
model: string,
|
|
391
|
+
operation: PolicyOperationKind,
|
|
392
|
+
context: QueryContext
|
|
393
|
+
) {
|
|
394
|
+
const db = (this.service.db as any)[model];
|
|
395
|
+
|
|
396
|
+
// check if the record is readable concerning "delete" policy
|
|
397
|
+
const readArgs = await this.queryProcessor.processQueryArgs(
|
|
398
|
+
model,
|
|
399
|
+
{ where: { id } },
|
|
400
|
+
operation,
|
|
401
|
+
context
|
|
402
|
+
);
|
|
403
|
+
console.log(
|
|
404
|
+
`Finding pre-operation ${model}:\n${JSON.stringify(readArgs)}`
|
|
405
|
+
);
|
|
406
|
+
const read = await db.findFirst(readArgs);
|
|
407
|
+
if (!read) {
|
|
408
|
+
throw new RequestHandlerError(
|
|
409
|
+
ServerErrorCode.DENIED_BY_POLICY,
|
|
410
|
+
'denied by policy'
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return read;
|
|
414
|
+
}
|
|
415
|
+
}
|