@fitzzero/quickdraw-core 2.3.0 → 3.1.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/eslint-config/index.mjs
CHANGED
|
@@ -101,7 +101,7 @@ export const server = [
|
|
|
101
101
|
},
|
|
102
102
|
plugins: { quickdraw: quickdrawPlugin },
|
|
103
103
|
rules: {
|
|
104
|
-
"quickdraw/no-
|
|
104
|
+
"quickdraw/no-cross-service-mutations": "warn",
|
|
105
105
|
"quickdraw/require-zod-schema": "warn",
|
|
106
106
|
"quickdraw/no-service-method-record": "error",
|
|
107
107
|
"quickdraw/no-unsafe-payload-cast": "warn",
|
|
@@ -121,12 +121,6 @@ export const server = [
|
|
|
121
121
|
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
|
-
{
|
|
125
|
-
files: ["**/services/**/index.ts"],
|
|
126
|
-
rules: {
|
|
127
|
-
"quickdraw/no-direct-prisma-mutations": "off",
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
124
|
];
|
|
131
125
|
|
|
132
126
|
export const client = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import noCrossServiceMutations from "./rules/no-cross-service-mutations.mjs"
|
|
2
2
|
import requireZodSchema from "./rules/require-zod-schema.mjs"
|
|
3
3
|
import noServiceMethodRecord from "./rules/no-service-method-record.mjs"
|
|
4
4
|
import noUnsafePayloadCast from "./rules/no-unsafe-payload-cast.mjs"
|
|
@@ -6,7 +6,7 @@ import noUnsafePayloadCast from "./rules/no-unsafe-payload-cast.mjs"
|
|
|
6
6
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
7
7
|
export default {
|
|
8
8
|
rules: {
|
|
9
|
-
"no-
|
|
9
|
+
"no-cross-service-mutations": noCrossServiceMutations,
|
|
10
10
|
"require-zod-schema": requireZodSchema,
|
|
11
11
|
"no-service-method-record": noServiceMethodRecord,
|
|
12
12
|
"no-unsafe-payload-cast": noUnsafePayloadCast,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const MUTATION_METHODS = new Set(["create", "update", "delete"])
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract the service directory name from a file path.
|
|
5
|
+
* e.g. "/app/services/cloud-build/index.ts" → "cloud-build"
|
|
6
|
+
* "/app/services/task/helpers.ts" → "task"
|
|
7
|
+
*/
|
|
8
|
+
function getServiceDirName(filename) {
|
|
9
|
+
const match = filename.match(/services\/([^/]+)\//)
|
|
10
|
+
return match ? match[1] : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert a kebab-case or snake_case directory name to camelCase
|
|
15
|
+
* to match Prisma model names.
|
|
16
|
+
* e.g. "cloud-build" → "cloudBuild", "task" → "task"
|
|
17
|
+
*/
|
|
18
|
+
function dirNameToModelName(dirName) {
|
|
19
|
+
return dirName.replace(/[-_](\w)/g, (_, c) => c.toUpperCase())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
23
|
+
export default {
|
|
24
|
+
meta: {
|
|
25
|
+
type: "problem",
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
"Disallow Prisma mutations on models not owned by the current service. " +
|
|
29
|
+
"Services should route mutations through the owning service, not bypass it with direct Prisma calls.",
|
|
30
|
+
},
|
|
31
|
+
messages: {
|
|
32
|
+
useBaseService:
|
|
33
|
+
"Use this.{{ method }}() from BaseService instead of this.prisma.{{ model }}.{{ method }}(). " +
|
|
34
|
+
"BaseService methods auto-emit updates to subscribers.",
|
|
35
|
+
crossServiceMutation:
|
|
36
|
+
"Direct mutation on '{{ model }}' is not owned by this service. " +
|
|
37
|
+
"Route through the owning service instead of this.prisma.{{ model }}.{{ method }}().",
|
|
38
|
+
},
|
|
39
|
+
schema: [
|
|
40
|
+
{
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
allowedModels: {
|
|
44
|
+
type: "object",
|
|
45
|
+
description:
|
|
46
|
+
"Map of service directory name → array of foreign model names that are allowed. " +
|
|
47
|
+
'e.g. { "task": ["chat"], "project": ["agent", "projectAgent"] }',
|
|
48
|
+
additionalProperties: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: { type: "string" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
create(context) {
|
|
59
|
+
const filename = context.filename || context.getFilename()
|
|
60
|
+
const serviceDirName = getServiceDirName(filename)
|
|
61
|
+
|
|
62
|
+
if (!serviceDirName) return {}
|
|
63
|
+
|
|
64
|
+
const ownedModel = dirNameToModelName(serviceDirName)
|
|
65
|
+
const isIndexFile = filename.endsWith("/index.ts") || filename.endsWith("/index.js")
|
|
66
|
+
const options = context.options[0] || {}
|
|
67
|
+
const allowedModels = options.allowedModels || {}
|
|
68
|
+
const allowedForThisService = new Set(allowedModels[serviceDirName] || [])
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
CallExpression(node) {
|
|
72
|
+
const callee = node.callee
|
|
73
|
+
if (callee.type !== "MemberExpression") return
|
|
74
|
+
if (callee.property.type !== "Identifier") return
|
|
75
|
+
|
|
76
|
+
const methodName = callee.property.name
|
|
77
|
+
if (!MUTATION_METHODS.has(methodName)) return
|
|
78
|
+
|
|
79
|
+
const obj = callee.object
|
|
80
|
+
if (obj.type !== "MemberExpression") return
|
|
81
|
+
|
|
82
|
+
const parentObj = obj.object
|
|
83
|
+
if (parentObj.type !== "MemberExpression") return
|
|
84
|
+
if (parentObj.property.type !== "Identifier") return
|
|
85
|
+
if (parentObj.property.name !== "prisma") return
|
|
86
|
+
|
|
87
|
+
if (parentObj.object.type !== "ThisExpression") return
|
|
88
|
+
|
|
89
|
+
const modelName =
|
|
90
|
+
obj.property.type === "Identifier" ? obj.property.name : "model"
|
|
91
|
+
|
|
92
|
+
if (modelName === ownedModel) {
|
|
93
|
+
if (!isIndexFile) {
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: "useBaseService",
|
|
97
|
+
data: { method: methodName, model: modelName },
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (allowedForThisService.has(modelName)) return
|
|
104
|
+
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: "crossServiceMutation",
|
|
108
|
+
data: { method: methodName, model: modelName },
|
|
109
|
+
})
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitzzero/quickdraw-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Fast fullstack patterns for real-time applications with Socket.io and TanStack Query",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/shared/index.js",
|
|
@@ -42,13 +42,15 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsup",
|
|
44
44
|
"dev": "tsup --watch",
|
|
45
|
-
"typecheck": "
|
|
46
|
-
"lint": "
|
|
47
|
-
"lint:fix": "
|
|
45
|
+
"typecheck": "tsgo --noEmit",
|
|
46
|
+
"lint": "oxlint src",
|
|
47
|
+
"lint:fix": "oxlint --fix src",
|
|
48
|
+
"format": "oxfmt --write .",
|
|
49
|
+
"format:check": "oxfmt --check .",
|
|
48
50
|
"test": "vitest run",
|
|
49
51
|
"test:watch": "vitest",
|
|
50
52
|
"test:coverage": "vitest run --coverage",
|
|
51
|
-
"prepublishOnly": "
|
|
53
|
+
"prepublishOnly": "bun run build"
|
|
52
54
|
},
|
|
53
55
|
"keywords": [
|
|
54
56
|
"socket.io",
|
|
@@ -115,12 +117,12 @@
|
|
|
115
117
|
"@types/express": "^4.17.21",
|
|
116
118
|
"@types/node": "^20.11.0",
|
|
117
119
|
"@types/react": "^18.2.0",
|
|
118
|
-
"@typescript
|
|
119
|
-
"@typescript-eslint/parser": "^7.0.0",
|
|
120
|
+
"@typescript/native-preview": "^7.0.0-dev",
|
|
120
121
|
"@vitest/coverage-v8": "^1.3.0",
|
|
121
|
-
"eslint": "^8.56.0",
|
|
122
122
|
"express": "^4.18.2",
|
|
123
123
|
"jsdom": "^24.0.0",
|
|
124
|
+
"oxfmt": "^0.37.0",
|
|
125
|
+
"oxlint": "^1.52.0",
|
|
124
126
|
"react": "^18.2.0",
|
|
125
127
|
"react-dom": "^18.2.0",
|
|
126
128
|
"socket.io": "^4.7.4",
|