@fitzzero/quickdraw-core 2.0.0 → 2.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/dist/client/index.d.ts +97 -1
- package/dist/client/index.js +210 -1
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.d.ts +160 -1
- package/dist/server/index.js +675 -1
- package/dist/server/index.js.map +1 -1
- package/eslint-plugin-quickdraw/index.mjs +14 -0
- package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +49 -0
- package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +35 -0
- package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +41 -0
- package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +60 -0
- package/package.json +6 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import noDirectPrismaMutations from "./rules/no-direct-prisma-mutations.mjs"
|
|
2
|
+
import requireZodSchema from "./rules/require-zod-schema.mjs"
|
|
3
|
+
import noServiceMethodRecord from "./rules/no-service-method-record.mjs"
|
|
4
|
+
import noUnsafePayloadCast from "./rules/no-unsafe-payload-cast.mjs"
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').ESLint.Plugin} */
|
|
7
|
+
export default {
|
|
8
|
+
rules: {
|
|
9
|
+
"no-direct-prisma-mutations": noDirectPrismaMutations,
|
|
10
|
+
"require-zod-schema": requireZodSchema,
|
|
11
|
+
"no-service-method-record": noServiceMethodRecord,
|
|
12
|
+
"no-unsafe-payload-cast": noUnsafePayloadCast,
|
|
13
|
+
},
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Disallow direct Prisma create/update/delete in service methods. Use this.create(), this.update(), this.delete() instead for proper event emission.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
noDirectMutation:
|
|
11
|
+
"Use this.{{ method }}() from BaseService instead of this.prisma.{{ model }}.{{ method }}(). " +
|
|
12
|
+
"BaseService methods auto-emit updates to subscribers.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const MUTATION_METHODS = new Set(["create", "update", "delete"])
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
const callee = node.callee
|
|
22
|
+
if (callee.type !== "MemberExpression") return
|
|
23
|
+
if (callee.property.type !== "Identifier") return
|
|
24
|
+
|
|
25
|
+
const methodName = callee.property.name
|
|
26
|
+
if (!MUTATION_METHODS.has(methodName)) return
|
|
27
|
+
|
|
28
|
+
const obj = callee.object
|
|
29
|
+
if (obj.type !== "MemberExpression") return
|
|
30
|
+
|
|
31
|
+
const parentObj = obj.object
|
|
32
|
+
if (parentObj.type !== "MemberExpression") return
|
|
33
|
+
if (parentObj.property.type !== "Identifier") return
|
|
34
|
+
if (parentObj.property.name !== "prisma") return
|
|
35
|
+
|
|
36
|
+
if (parentObj.object.type !== "ThisExpression") return
|
|
37
|
+
|
|
38
|
+
const modelName =
|
|
39
|
+
obj.property.type === "Identifier" ? obj.property.name : "model"
|
|
40
|
+
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: "noDirectMutation",
|
|
44
|
+
data: { method: methodName, model: modelName },
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Disallow using & ServiceMethodsRecord intersection on BaseService generics. " +
|
|
8
|
+
"This escape hatch defeats type safety by allowing any method name.",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noServiceMethodRecord:
|
|
12
|
+
"Remove '& ServiceMethodsRecord' from the BaseService generic. " +
|
|
13
|
+
"Use the concrete service methods type directly for full type safety.",
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
TSIntersectionType(node) {
|
|
20
|
+
for (const member of node.types) {
|
|
21
|
+
if (
|
|
22
|
+
member.type === "TSTypeReference" &&
|
|
23
|
+
member.typeName?.type === "Identifier" &&
|
|
24
|
+
member.typeName.name === "ServiceMethodsRecord"
|
|
25
|
+
) {
|
|
26
|
+
context.report({
|
|
27
|
+
node: member,
|
|
28
|
+
messageId: "noServiceMethodRecord",
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "suggestion",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Warn on 'as z.infer<typeof ...>' casts in service handlers. " +
|
|
8
|
+
"These are usually unnecessary when types flow correctly from defineMethod.",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noUnsafePayloadCast:
|
|
12
|
+
"Avoid casting payload with 'as z.infer<typeof ...>'. " +
|
|
13
|
+
"The payload should be properly typed through defineMethod's generic constraint.",
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
TSAsExpression(node) {
|
|
20
|
+
const typeAnnotation = node.typeAnnotation
|
|
21
|
+
if (typeAnnotation.type !== "TSTypeReference") return
|
|
22
|
+
if (typeAnnotation.typeName?.type !== "TSQualifiedName") return
|
|
23
|
+
|
|
24
|
+
const left = typeAnnotation.typeName.left
|
|
25
|
+
const right = typeAnnotation.typeName.right
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
left.type === "Identifier" &&
|
|
29
|
+
left.name === "z" &&
|
|
30
|
+
right.type === "Identifier" &&
|
|
31
|
+
right.name === "infer"
|
|
32
|
+
) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "noUnsafePayloadCast",
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "suggestion",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
"Warn when defineMethod() is called without a Zod schema option. All user-facing methods should validate payloads.",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
missingSchema:
|
|
11
|
+
"defineMethod('{{ methodName }}') is missing a Zod schema. " +
|
|
12
|
+
"Add { schema: mySchema } as the 4th argument for payload validation.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
const callee = node.callee
|
|
20
|
+
if (callee.type !== "MemberExpression") return
|
|
21
|
+
if (callee.property.type !== "Identifier") return
|
|
22
|
+
if (callee.property.name !== "defineMethod") return
|
|
23
|
+
if (callee.object.type !== "ThisExpression") return
|
|
24
|
+
|
|
25
|
+
const methodNameArg = node.arguments[0]
|
|
26
|
+
const methodName =
|
|
27
|
+
methodNameArg?.type === "Literal"
|
|
28
|
+
? String(methodNameArg.value)
|
|
29
|
+
: "unknown"
|
|
30
|
+
|
|
31
|
+
const optionsArg = node.arguments[3]
|
|
32
|
+
|
|
33
|
+
if (!optionsArg) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "missingSchema",
|
|
37
|
+
data: { methodName },
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (optionsArg.type === "ObjectExpression") {
|
|
43
|
+
const hasSchema = optionsArg.properties.some(
|
|
44
|
+
(p) =>
|
|
45
|
+
p.type === "Property" &&
|
|
46
|
+
p.key.type === "Identifier" &&
|
|
47
|
+
p.key.name === "schema"
|
|
48
|
+
)
|
|
49
|
+
if (!hasSchema) {
|
|
50
|
+
context.report({
|
|
51
|
+
node,
|
|
52
|
+
messageId: "missingSchema",
|
|
53
|
+
data: { methodName },
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitzzero/quickdraw-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.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",
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
"./client/testing": {
|
|
27
27
|
"import": "./dist/client/testing.js",
|
|
28
28
|
"types": "./dist/client/testing.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./eslint-plugin": {
|
|
31
|
+
"import": "./eslint-plugin-quickdraw/index.mjs"
|
|
29
32
|
}
|
|
30
33
|
},
|
|
31
34
|
"files": [
|
|
32
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"eslint-plugin-quickdraw"
|
|
33
37
|
],
|
|
34
38
|
"scripts": {
|
|
35
39
|
"build": "tsup",
|