@fitzzero/quickdraw-core 1.2.1 → 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.
@@ -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": "1.2.1",
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,11 +26,26 @@
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
  ],
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "eslint src --ext .ts,.tsx",
43
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "test:coverage": "vitest run --coverage",
47
+ "prepublishOnly": "pnpm build"
48
+ },
34
49
  "keywords": [
35
50
  "socket.io",
36
51
  "tanstack-query",
@@ -96,15 +111,5 @@
96
111
  },
97
112
  "engines": {
98
113
  "node": ">=20"
99
- },
100
- "scripts": {
101
- "build": "tsup",
102
- "dev": "tsup --watch",
103
- "typecheck": "tsc --noEmit",
104
- "lint": "eslint src --ext .ts,.tsx",
105
- "lint:fix": "eslint src --ext .ts,.tsx --fix",
106
- "test": "vitest run",
107
- "test:watch": "vitest",
108
- "test:coverage": "vitest run --coverage"
109
114
  }
110
- }
115
+ }