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.
Files changed (215) hide show
  1. package/.vscode/extensions.json +7 -0
  2. package/.vscode/launch.json +49 -0
  3. package/.vscode/settings.json +4 -0
  4. package/README.md +1 -0
  5. package/package.json +8 -90
  6. package/packages/internal/jest.config.ts +32 -0
  7. package/packages/internal/package.json +42 -0
  8. package/packages/internal/src/constants.ts +1 -0
  9. package/packages/internal/src/handler/data/guard-utils.ts +7 -0
  10. package/packages/internal/src/handler/data/handler.ts +415 -0
  11. package/packages/internal/src/handler/data/query-processor.ts +504 -0
  12. package/packages/internal/src/handler/index.ts +1 -0
  13. package/packages/internal/src/handler/types.ts +20 -0
  14. package/packages/internal/src/index.ts +3 -0
  15. package/packages/internal/src/request-handler.ts +27 -0
  16. package/packages/internal/src/request.ts +101 -0
  17. package/packages/internal/src/types.ts +40 -0
  18. package/packages/internal/tests/query-processor.test.ts +172 -0
  19. package/{out/cli/tsconfig.template.json → packages/internal/tsconfig.json} +7 -3
  20. package/packages/runtime/auth.d.ts +1 -0
  21. package/packages/runtime/auth.js +3 -0
  22. package/packages/runtime/hooks.d.ts +10 -0
  23. package/packages/runtime/hooks.js +3 -0
  24. package/packages/runtime/index.d.ts +3 -0
  25. package/packages/runtime/index.js +1 -0
  26. package/packages/runtime/package-lock.json +512 -0
  27. package/packages/runtime/package.json +16 -0
  28. package/packages/runtime/server.d.ts +1 -0
  29. package/packages/runtime/server.js +3 -0
  30. package/packages/runtime/types.d.ts +1 -0
  31. package/packages/runtime/types.js +3 -0
  32. package/packages/schema/.eslintrc.json +13 -0
  33. package/packages/schema/.vscodeignore +4 -0
  34. package/packages/schema/asset/logo-dark.png +0 -0
  35. package/packages/schema/asset/logo-light.png +0 -0
  36. package/{bin → packages/schema/bin}/cli +0 -0
  37. package/packages/schema/jest.config.ts +32 -0
  38. package/packages/schema/langium-config.json +14 -0
  39. package/packages/schema/langium-quickstart.md +41 -0
  40. package/packages/schema/language-configuration.json +30 -0
  41. package/packages/schema/package.json +96 -0
  42. package/packages/schema/src/cli/cli-util.ts +80 -0
  43. package/packages/schema/src/cli/index.ts +64 -0
  44. package/packages/schema/src/extension.ts +76 -0
  45. package/packages/schema/src/generator/constants.ts +5 -0
  46. package/packages/schema/src/generator/index.ts +92 -0
  47. package/{out/generator/next-auth/index.js → packages/schema/src/generator/next-auth/index.ts} +46 -58
  48. package/{out → packages/schema/src}/generator/package.template.json +0 -0
  49. package/packages/schema/src/generator/prisma/expression-writer.ts +352 -0
  50. package/packages/schema/src/generator/prisma/index.ts +32 -0
  51. package/packages/schema/src/generator/prisma/plain-expression-builder.ts +91 -0
  52. package/packages/schema/src/generator/prisma/prisma-builder.ts +366 -0
  53. package/packages/schema/src/generator/prisma/query-gard-generator.ts +208 -0
  54. package/packages/schema/src/generator/prisma/schema-generator.ts +300 -0
  55. package/packages/schema/src/generator/react-hooks/index.ts +181 -0
  56. package/packages/schema/src/generator/service/index.ts +107 -0
  57. package/{out → packages/schema/src}/generator/tsconfig.template.json +0 -0
  58. package/packages/schema/src/generator/types.ts +17 -0
  59. package/packages/schema/src/generator/utils.ts +9 -0
  60. package/packages/schema/src/language-server/generated/ast.ts +603 -0
  61. package/{out/language-server/generated/grammar.js → packages/schema/src/language-server/generated/grammar.ts} +5 -8
  62. package/packages/schema/src/language-server/generated/module.ts +24 -0
  63. package/packages/schema/src/language-server/main.ts +12 -0
  64. package/{out → packages/schema/src}/language-server/stdlib.zmodel +0 -0
  65. package/packages/schema/src/language-server/types.ts +9 -0
  66. package/packages/schema/src/language-server/zmodel-index.ts +33 -0
  67. package/packages/schema/src/language-server/zmodel-linker.ts +409 -0
  68. package/packages/schema/src/language-server/zmodel-module.ts +90 -0
  69. package/packages/schema/src/language-server/zmodel-scope.ts +21 -0
  70. package/packages/schema/src/language-server/zmodel-validator.ts +35 -0
  71. package/packages/schema/src/language-server/zmodel.langium +186 -0
  72. package/packages/schema/src/utils/exec-utils.ts +5 -0
  73. package/packages/schema/src/utils/indent-string.ts +6 -0
  74. package/packages/schema/syntaxes/zmodel.json +57 -0
  75. package/packages/schema/syntaxes/zmodel.tmLanguage.json +57 -0
  76. package/packages/schema/tests/generator/expression-writer.test.ts +676 -0
  77. package/packages/schema/tests/generator/prisma-builder.test.ts +138 -0
  78. package/packages/schema/tests/schema/parser.test.ts +423 -0
  79. package/packages/schema/tests/schema/sample-todo.test.ts +14 -0
  80. package/packages/schema/tests/utils.ts +38 -0
  81. package/packages/schema/tsconfig.json +23 -0
  82. package/pnpm-workspace.yaml +3 -0
  83. package/samples/todo/.env +2 -0
  84. package/samples/todo/.eslintrc.json +3 -0
  85. package/samples/todo/.vscode/launch.json +11 -0
  86. package/samples/todo/README.md +34 -0
  87. package/samples/todo/components/AuthGuard.tsx +17 -0
  88. package/samples/todo/components/Avatar.tsx +22 -0
  89. package/samples/todo/components/BreadCrumb.tsx +44 -0
  90. package/samples/todo/components/ManageMembers.tsx +134 -0
  91. package/samples/todo/components/NavBar.tsx +57 -0
  92. package/samples/todo/components/SpaceMembers.tsx +76 -0
  93. package/samples/todo/components/Spaces.tsx +28 -0
  94. package/samples/todo/components/TimeInfo.tsx +17 -0
  95. package/samples/todo/components/Todo.tsx +72 -0
  96. package/samples/todo/components/TodoList.tsx +77 -0
  97. package/samples/todo/lib/context.ts +31 -0
  98. package/samples/todo/next.config.js +10 -0
  99. package/samples/todo/package-lock.json +7527 -0
  100. package/samples/todo/package.json +45 -0
  101. package/samples/todo/pages/_app.tsx +50 -0
  102. package/samples/todo/pages/api/auth/[...nextauth].ts +83 -0
  103. package/samples/todo/pages/api/zenstack/[...path].ts +16 -0
  104. package/samples/todo/pages/create-space.tsx +114 -0
  105. package/samples/todo/pages/index.tsx +32 -0
  106. package/samples/todo/pages/space/[slug]/[listId]/index.tsx +88 -0
  107. package/samples/todo/pages/space/[slug]/index.tsx +169 -0
  108. package/samples/todo/postcss.config.js +6 -0
  109. package/samples/todo/public/avatar.jpg +0 -0
  110. package/samples/todo/public/favicon.ico +0 -0
  111. package/samples/todo/public/logo.png +0 -0
  112. package/samples/todo/public/vercel.svg +4 -0
  113. package/samples/todo/styles/globals.css +7 -0
  114. package/samples/todo/tailwind.config.js +11 -0
  115. package/samples/todo/tsconfig.json +28 -0
  116. package/samples/todo/types/next-auth.d.ts +14 -0
  117. package/samples/todo/types/next.d.ts +16 -0
  118. package/samples/todo/zenstack/migrations/20221014084317_init/migration.sql +153 -0
  119. package/samples/todo/zenstack/migrations/20221020094651_upate_cli/migration.sql +23 -0
  120. package/samples/todo/zenstack/migrations/migration_lock.toml +3 -0
  121. package/samples/todo/zenstack/schema.prisma +126 -0
  122. package/samples/todo/zenstack/schema.zmodel +161 -0
  123. package/tests/integration/jest.config.ts +16 -0
  124. package/tests/integration/package-lock.json +1081 -0
  125. package/tests/integration/package.json +27 -0
  126. package/tests/integration/tests/operation-coverate.test.ts +563 -0
  127. package/tests/integration/tests/operations.zmodel +69 -0
  128. package/tests/integration/tests/todo-e2e.test.ts +577 -0
  129. package/tests/integration/tests/todo.zmodel +123 -0
  130. package/tests/integration/tests/tsconfig.template.json +10 -0
  131. package/tests/integration/tests/utils.ts +133 -0
  132. package/tests/integration/tsconfig.json +10 -0
  133. package/out/cli/cli-util.js +0 -64
  134. package/out/cli/cli-util.js.map +0 -1
  135. package/out/cli/generator.js +0 -1
  136. package/out/cli/generator.js.map +0 -1
  137. package/out/cli/index.js +0 -46
  138. package/out/cli/index.js.map +0 -1
  139. package/out/cli/package.template.json +0 -10
  140. package/out/extension.js +0 -81
  141. package/out/extension.js.map +0 -1
  142. package/out/generator/constants.js +0 -9
  143. package/out/generator/constants.js.map +0 -1
  144. package/out/generator/data-server/index.js +0 -1
  145. package/out/generator/data-server/index.js.map +0 -1
  146. package/out/generator/index.js +0 -98
  147. package/out/generator/index.js.map +0 -1
  148. package/out/generator/next-auth/index.js.map +0 -1
  149. package/out/generator/prisma/expression-writer.js +0 -287
  150. package/out/generator/prisma/expression-writer.js.map +0 -1
  151. package/out/generator/prisma/index.js +0 -38
  152. package/out/generator/prisma/index.js.map +0 -1
  153. package/out/generator/prisma/plain-expression-builder.js +0 -69
  154. package/out/generator/prisma/plain-expression-builder.js.map +0 -1
  155. package/out/generator/prisma/prisma-builder.js +0 -307
  156. package/out/generator/prisma/prisma-builder.js.map +0 -1
  157. package/out/generator/prisma/query-gard-generator.js +0 -159
  158. package/out/generator/prisma/query-gard-generator.js.map +0 -1
  159. package/out/generator/prisma/schema-generator.js +0 -201
  160. package/out/generator/prisma/schema-generator.js.map +0 -1
  161. package/out/generator/query-guard/index.js +0 -2
  162. package/out/generator/query-guard/index.js.map +0 -1
  163. package/out/generator/react-hooks/index.js +0 -179
  164. package/out/generator/react-hooks/index.js.map +0 -1
  165. package/out/generator/server/data/data-generator.js +0 -376
  166. package/out/generator/server/data/data-generator.js.map +0 -1
  167. package/out/generator/server/data/expression-writer.js +0 -287
  168. package/out/generator/server/data/expression-writer.js.map +0 -1
  169. package/out/generator/server/data/plain-expression-builder.js +0 -69
  170. package/out/generator/server/data/plain-expression-builder.js.map +0 -1
  171. package/out/generator/server/data-generator.js +0 -82
  172. package/out/generator/server/data-generator.js.map +0 -1
  173. package/out/generator/server/expression-writer.js +0 -1
  174. package/out/generator/server/expression-writer.js.map +0 -1
  175. package/out/generator/server/function/function-generator.js +0 -50
  176. package/out/generator/server/function/function-generator.js.map +0 -1
  177. package/out/generator/server/function-generator.js +0 -13
  178. package/out/generator/server/function-generator.js.map +0 -1
  179. package/out/generator/server/index.js +0 -88
  180. package/out/generator/server/index.js.map +0 -1
  181. package/out/generator/server/js-expression-builder.js +0 -1
  182. package/out/generator/server/js-expression-builder.js.map +0 -1
  183. package/out/generator/server/plain-expression-builder.js +0 -1
  184. package/out/generator/server/plain-expression-builder.js.map +0 -1
  185. package/out/generator/server/server-code-generator.js +0 -3
  186. package/out/generator/server/server-code-generator.js.map +0 -1
  187. package/out/generator/server/server-code-writer.js +0 -1
  188. package/out/generator/server/server-code-writer.js.map +0 -1
  189. package/out/generator/service/index.js +0 -133
  190. package/out/generator/service/index.js.map +0 -1
  191. package/out/generator/types.js +0 -10
  192. package/out/generator/types.js.map +0 -1
  193. package/out/generator/utils.js +0 -10
  194. package/out/generator/utils.js.map +0 -1
  195. package/out/language-server/generated/ast.js +0 -386
  196. package/out/language-server/generated/ast.js.map +0 -1
  197. package/out/language-server/generated/grammar.js.map +0 -1
  198. package/out/language-server/generated/module.js +0 -23
  199. package/out/language-server/generated/module.js.map +0 -1
  200. package/out/language-server/main.js +0 -12
  201. package/out/language-server/main.js.map +0 -1
  202. package/out/language-server/types.js +0 -3
  203. package/out/language-server/types.js.map +0 -1
  204. package/out/language-server/zmodel-index.js +0 -38
  205. package/out/language-server/zmodel-index.js.map +0 -1
  206. package/out/language-server/zmodel-linker.js +0 -241
  207. package/out/language-server/zmodel-linker.js.map +0 -1
  208. package/out/language-server/zmodel-module.js +0 -51
  209. package/out/language-server/zmodel-module.js.map +0 -1
  210. package/out/language-server/zmodel-scope.js +0 -30
  211. package/out/language-server/zmodel-scope.js.map +0 -1
  212. package/out/language-server/zmodel-validator.js +0 -25
  213. package/out/language-server/zmodel-validator.js.map +0 -1
  214. package/out/utils/indent-string.js +0 -9
  215. 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
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "npm.packageManager": "pnpm",
3
+ "eslint.packageManager": "pnpm"
4
+ }
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
- "displayName": "ZenStack CLI and Language Tools",
4
- "description": "ZenStack CLI and Language Tools",
5
- "version": "0.1.42",
6
- "engines": {
7
- "vscode": "^1.56.0"
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
- "vscode:prepublish": "npm run build && npm run lint",
86
- "build": "tsc && tsc-alias && cp src/language-server/stdlib.zmodel ./out/language-server/ && cp src/generator/*.template.json ./out/generator/",
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,7 @@
1
+ export function and(condition1: any, condition2: any) {
2
+ if (condition1 && condition2) {
3
+ return { AND: [condition1, condition2] };
4
+ } else {
5
+ return condition1 ?? condition2;
6
+ }
7
+ }
@@ -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
+ }