@tailor-platform/erp-kit 0.0.1 → 0.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.
Files changed (213) hide show
  1. package/README.md +196 -28
  2. package/dist/cli.js +894 -0
  3. package/package.json +65 -8
  4. package/rules/app-compose/backend/auth.md +78 -0
  5. package/rules/app-compose/frontend/auth.md +55 -0
  6. package/rules/app-compose/frontend/component.md +55 -0
  7. package/rules/app-compose/frontend/page.md +86 -0
  8. package/rules/app-compose/frontend/screen-detailview.md +112 -0
  9. package/rules/app-compose/frontend/screen-form.md +145 -0
  10. package/rules/app-compose/frontend/screen-listview.md +159 -0
  11. package/rules/app-compose/structure.md +32 -0
  12. package/rules/module-development/commands.md +54 -0
  13. package/rules/module-development/cross-module-type-injection.md +28 -0
  14. package/rules/module-development/dependency-modules.md +24 -0
  15. package/rules/module-development/errors.md +12 -0
  16. package/rules/module-development/executors.md +67 -0
  17. package/rules/module-development/exports.md +13 -0
  18. package/rules/module-development/models.md +34 -0
  19. package/rules/module-development/structure.md +27 -0
  20. package/rules/module-development/sync-vs-async-operations.md +83 -0
  21. package/rules/module-development/testing.md +43 -0
  22. package/rules/sdk-best-practices/db-relations.md +74 -0
  23. package/rules/sdk-best-practices/sdk-docs.md +14 -0
  24. package/schemas/app-compose/actors.yml +34 -0
  25. package/schemas/app-compose/business-flow.yml +50 -0
  26. package/schemas/app-compose/requirements.yml +33 -0
  27. package/schemas/app-compose/resolver.yml +47 -0
  28. package/schemas/app-compose/screen.yml +81 -0
  29. package/schemas/app-compose/story.yml +67 -0
  30. package/schemas/module/command.yml +52 -0
  31. package/schemas/module/feature.yml +58 -0
  32. package/schemas/module/model.yml +70 -0
  33. package/schemas/module/module.yml +50 -0
  34. package/skills/1-module-docs/SKILL.md +107 -0
  35. package/skills/2-module-feature-breakdown/SKILL.md +66 -0
  36. package/skills/3-module-doc-review/SKILL.md +230 -0
  37. package/skills/4-module-tdd-implementation/SKILL.md +56 -0
  38. package/skills/5-module-implementation-review/SKILL.md +400 -0
  39. package/skills/app-compose-1-requirement-analysis/SKILL.md +85 -0
  40. package/skills/app-compose-2-requirements-breakdown/SKILL.md +88 -0
  41. package/skills/app-compose-3-doc-review/SKILL.md +112 -0
  42. package/skills/app-compose-4-design-mock/SKILL.md +248 -0
  43. package/skills/app-compose-5-design-mock-review/SKILL.md +283 -0
  44. package/skills/app-compose-6-implementation-spec/SKILL.md +122 -0
  45. package/skills/mock-scenario/SKILL.md +118 -0
  46. package/src/app.ts +1 -0
  47. package/src/cli.ts +120 -0
  48. package/src/commands/check.test.ts +30 -0
  49. package/src/commands/check.ts +66 -0
  50. package/src/commands/init.test.ts +77 -0
  51. package/src/commands/init.ts +87 -0
  52. package/src/commands/mock/index.ts +53 -0
  53. package/src/commands/mock/start.ts +179 -0
  54. package/src/commands/mock/validate.test.ts +185 -0
  55. package/src/commands/mock/validate.ts +198 -0
  56. package/src/commands/scaffold.test.ts +76 -0
  57. package/src/commands/scaffold.ts +119 -0
  58. package/src/commands/sync-check.test.ts +125 -0
  59. package/src/commands/sync-check.ts +182 -0
  60. package/src/integration.test.ts +63 -0
  61. package/src/mdschema.ts +48 -0
  62. package/src/mockServer.ts +55 -0
  63. package/src/module.ts +86 -0
  64. package/src/modules/accounting/.gitkeep +0 -0
  65. package/src/modules/coa-management/.gitkeep +0 -0
  66. package/src/modules/inventory/.gitkeep +0 -0
  67. package/src/modules/manufacturing/.gitkeep +0 -0
  68. package/src/modules/primitives/README.md +39 -0
  69. package/src/modules/primitives/command/activateCategory.test.ts +75 -0
  70. package/src/modules/primitives/command/activateCategory.ts +50 -0
  71. package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
  72. package/src/modules/primitives/command/activateCurrency.ts +50 -0
  73. package/src/modules/primitives/command/activateUnit.test.ts +53 -0
  74. package/src/modules/primitives/command/activateUnit.ts +50 -0
  75. package/src/modules/primitives/command/convertAmount.test.ts +275 -0
  76. package/src/modules/primitives/command/convertAmount.ts +126 -0
  77. package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
  78. package/src/modules/primitives/command/convertQuantity.ts +73 -0
  79. package/src/modules/primitives/command/createCategory.test.ts +126 -0
  80. package/src/modules/primitives/command/createCategory.ts +89 -0
  81. package/src/modules/primitives/command/createCurrency.test.ts +191 -0
  82. package/src/modules/primitives/command/createCurrency.ts +77 -0
  83. package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
  84. package/src/modules/primitives/command/createExchangeRate.ts +91 -0
  85. package/src/modules/primitives/command/createUnit.test.ts +214 -0
  86. package/src/modules/primitives/command/createUnit.ts +88 -0
  87. package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
  88. package/src/modules/primitives/command/deactivateCategory.ts +62 -0
  89. package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
  90. package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
  91. package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
  92. package/src/modules/primitives/command/deactivateUnit.ts +62 -0
  93. package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
  94. package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
  95. package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
  96. package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
  97. package/src/modules/primitives/db/currency.ts +30 -0
  98. package/src/modules/primitives/db/exchangeRate.ts +28 -0
  99. package/src/modules/primitives/db/unit.ts +32 -0
  100. package/src/modules/primitives/db/uomCategory.ts +32 -0
  101. package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
  102. package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
  103. package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
  104. package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
  105. package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
  106. package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
  107. package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
  108. package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
  109. package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
  110. package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
  111. package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
  112. package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
  113. package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
  114. package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
  115. package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
  116. package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
  117. package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
  118. package/src/modules/primitives/docs/features/uom-categories.md +52 -0
  119. package/src/modules/primitives/docs/models/Currency.md +45 -0
  120. package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
  121. package/src/modules/primitives/docs/models/Unit.md +46 -0
  122. package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
  123. package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
  124. package/src/modules/primitives/index.ts +40 -0
  125. package/src/modules/primitives/lib/errors.ts +138 -0
  126. package/src/modules/primitives/lib/types.ts +20 -0
  127. package/src/modules/primitives/module.ts +66 -0
  128. package/src/modules/primitives/permissions.ts +18 -0
  129. package/src/modules/primitives/tailor.config.ts +11 -0
  130. package/src/modules/primitives/testing/fixtures.ts +161 -0
  131. package/src/modules/product-management/.gitkeep +0 -0
  132. package/src/modules/purchase/.gitkeep +0 -0
  133. package/src/modules/sales/.gitkeep +0 -0
  134. package/src/modules/shared/createContext.test.ts +39 -0
  135. package/src/modules/shared/createContext.ts +15 -0
  136. package/src/modules/shared/defineCommand.test.ts +42 -0
  137. package/src/modules/shared/defineCommand.ts +19 -0
  138. package/src/modules/shared/definePermissions.test.ts +146 -0
  139. package/src/modules/shared/definePermissions.ts +94 -0
  140. package/src/modules/shared/entityTypes.ts +15 -0
  141. package/src/modules/shared/errors.ts +22 -0
  142. package/src/modules/shared/index.ts +1 -0
  143. package/src/modules/shared/internal.ts +13 -0
  144. package/src/modules/shared/requirePermission.test.ts +47 -0
  145. package/src/modules/shared/requirePermission.ts +8 -0
  146. package/src/modules/shared/types.ts +4 -0
  147. package/src/modules/supplier-management/.gitkeep +0 -0
  148. package/src/modules/supplier-portal/.gitkeep +0 -0
  149. package/src/modules/testing/index.ts +120 -0
  150. package/src/modules/user-management/README.md +38 -0
  151. package/src/modules/user-management/command/activateUser.test.ts +112 -0
  152. package/src/modules/user-management/command/activateUser.ts +67 -0
  153. package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
  154. package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
  155. package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
  156. package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
  157. package/src/modules/user-management/command/createPermission.test.ts +143 -0
  158. package/src/modules/user-management/command/createPermission.ts +66 -0
  159. package/src/modules/user-management/command/createRole.test.ts +115 -0
  160. package/src/modules/user-management/command/createRole.ts +52 -0
  161. package/src/modules/user-management/command/createUser.test.ts +198 -0
  162. package/src/modules/user-management/command/createUser.ts +85 -0
  163. package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
  164. package/src/modules/user-management/command/deactivateUser.ts +67 -0
  165. package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
  166. package/src/modules/user-management/command/logAuditEvent.ts +59 -0
  167. package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
  168. package/src/modules/user-management/command/reactivateUser.ts +67 -0
  169. package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
  170. package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
  171. package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
  172. package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
  173. package/src/modules/user-management/db/auditEvent.ts +47 -0
  174. package/src/modules/user-management/db/permission.ts +31 -0
  175. package/src/modules/user-management/db/role.ts +28 -0
  176. package/src/modules/user-management/db/rolePermission.ts +44 -0
  177. package/src/modules/user-management/db/user.ts +38 -0
  178. package/src/modules/user-management/db/userRole.ts +44 -0
  179. package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
  180. package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
  181. package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
  182. package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
  183. package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
  184. package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
  185. package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
  186. package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
  187. package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
  188. package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
  189. package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
  190. package/src/modules/user-management/docs/features/audit-trail.md +80 -0
  191. package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
  192. package/src/modules/user-management/docs/features/user-account-management.md +64 -0
  193. package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
  194. package/src/modules/user-management/docs/models/Permission.md +31 -0
  195. package/src/modules/user-management/docs/models/Role.md +31 -0
  196. package/src/modules/user-management/docs/models/RolePermission.md +33 -0
  197. package/src/modules/user-management/docs/models/User.md +47 -0
  198. package/src/modules/user-management/docs/models/UserRole.md +34 -0
  199. package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
  200. package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
  201. package/src/modules/user-management/generated/enums.ts +24 -0
  202. package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
  203. package/src/modules/user-management/index.ts +32 -0
  204. package/src/modules/user-management/lib/errors.ts +81 -0
  205. package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
  206. package/src/modules/user-management/lib/types.ts +31 -0
  207. package/src/modules/user-management/module.ts +77 -0
  208. package/src/modules/user-management/permissions.ts +15 -0
  209. package/src/modules/user-management/tailor.config.ts +11 -0
  210. package/src/modules/user-management/testing/fixtures.ts +98 -0
  211. package/src/schemas.ts +25 -0
  212. package/src/testing.ts +10 -0
  213. package/src/util.ts +3 -0
@@ -0,0 +1,161 @@
1
+ import type { Currency, ExchangeRate, Schema, UoMCategory, Unit } from "../lib/types";
2
+
3
+ export const baseUoMCategory = {
4
+ id: "category-1",
5
+ name: "Weight",
6
+ description: "Weight units",
7
+ referenceUnitId: "unit-kg",
8
+ isActive: true,
9
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
10
+ updatedAt: null,
11
+ } as const satisfies UoMCategory<Schema>;
12
+
13
+ export const baseUnitKg = {
14
+ id: "unit-kg",
15
+ name: "Kilogram",
16
+ symbol: "kg",
17
+ categoryId: "category-1",
18
+ conversionFactor: 1.0,
19
+ roundingPrecision: 2,
20
+ isActive: true,
21
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
22
+ updatedAt: null,
23
+ } as const satisfies Unit<Schema>;
24
+
25
+ export const baseUnitGram = {
26
+ id: "unit-g",
27
+ name: "Gram",
28
+ symbol: "g",
29
+ categoryId: "category-1",
30
+ conversionFactor: 0.001,
31
+ roundingPrecision: 0,
32
+ isActive: true,
33
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
34
+ updatedAt: null,
35
+ } as const satisfies Unit<Schema>;
36
+
37
+ export const baseUnitPound = {
38
+ id: "unit-lb",
39
+ name: "Pound",
40
+ symbol: "lb",
41
+ categoryId: "category-1",
42
+ conversionFactor: 0.453592,
43
+ roundingPrecision: 2,
44
+ isActive: true,
45
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
46
+ updatedAt: null,
47
+ } as const satisfies Unit<Schema>;
48
+
49
+ export const baseUnitLiter = {
50
+ id: "unit-l",
51
+ name: "Liter",
52
+ symbol: "L",
53
+ categoryId: "category-2",
54
+ conversionFactor: 1.0,
55
+ roundingPrecision: 2,
56
+ isActive: true,
57
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
58
+ updatedAt: null,
59
+ } as const satisfies Unit<Schema>;
60
+
61
+ export const inactiveUnit = {
62
+ id: "unit-inactive",
63
+ name: "Inactive Unit",
64
+ symbol: "iu",
65
+ categoryId: "category-1",
66
+ conversionFactor: 1.0,
67
+ roundingPrecision: 2,
68
+ isActive: false,
69
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
70
+ updatedAt: null,
71
+ } as const satisfies Unit<Schema>;
72
+
73
+ // Currency fixtures
74
+ export const baseCurrencyUSD = {
75
+ id: "currency-usd",
76
+ code: "USD",
77
+ name: "US Dollar",
78
+ symbol: "$",
79
+ decimalPlaces: 2,
80
+ isBaseCurrency: true,
81
+ isActive: true,
82
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
83
+ updatedAt: null,
84
+ } as const satisfies Currency<Schema>;
85
+
86
+ export const baseCurrencyEUR = {
87
+ id: "currency-eur",
88
+ code: "EUR",
89
+ name: "Euro",
90
+ symbol: "€",
91
+ decimalPlaces: 2,
92
+ isBaseCurrency: false,
93
+ isActive: true,
94
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
95
+ updatedAt: null,
96
+ } as const satisfies Currency<Schema>;
97
+
98
+ export const baseCurrencyJPY = {
99
+ id: "currency-jpy",
100
+ code: "JPY",
101
+ name: "Japanese Yen",
102
+ symbol: "¥",
103
+ decimalPlaces: 0,
104
+ isBaseCurrency: false,
105
+ isActive: true,
106
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
107
+ updatedAt: null,
108
+ } as const satisfies Currency<Schema>;
109
+
110
+ export const inactiveCurrency = {
111
+ id: "currency-inactive",
112
+ code: "XXX",
113
+ name: "Inactive Currency",
114
+ symbol: "X",
115
+ decimalPlaces: 2,
116
+ isBaseCurrency: false,
117
+ isActive: false,
118
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
119
+ updatedAt: null,
120
+ } as const satisfies Currency<Schema>;
121
+
122
+ // ExchangeRate fixtures
123
+ export const baseExchangeRateUSDtoEUR = {
124
+ id: "rate-usd-eur-1",
125
+ sourceCurrencyId: "currency-usd",
126
+ targetCurrencyId: "currency-eur",
127
+ rate: 0.92,
128
+ effectiveDate: new Date("2024-01-15T00:00:00.000Z"),
129
+ createdAt: new Date("2024-01-15T00:00:00.000Z"),
130
+ updatedAt: null,
131
+ } as const satisfies ExchangeRate<Schema>;
132
+
133
+ export const baseExchangeRateUSDtoJPY = {
134
+ id: "rate-usd-jpy-1",
135
+ sourceCurrencyId: "currency-usd",
136
+ targetCurrencyId: "currency-jpy",
137
+ rate: 148.5,
138
+ effectiveDate: new Date("2024-01-15T00:00:00.000Z"),
139
+ createdAt: new Date("2024-01-15T00:00:00.000Z"),
140
+ updatedAt: null,
141
+ } as const satisfies ExchangeRate<Schema>;
142
+
143
+ export const olderExchangeRateUSDtoEUR = {
144
+ id: "rate-usd-eur-old",
145
+ sourceCurrencyId: "currency-usd",
146
+ targetCurrencyId: "currency-eur",
147
+ rate: 0.85,
148
+ effectiveDate: new Date("2024-01-01T00:00:00.000Z"),
149
+ createdAt: new Date("2024-01-01T00:00:00.000Z"),
150
+ updatedAt: null,
151
+ } as const satisfies ExchangeRate<Schema>;
152
+
153
+ export const baseExchangeRateEURtoUSD = {
154
+ id: "rate-eur-usd-1",
155
+ sourceCurrencyId: "currency-eur",
156
+ targetCurrencyId: "currency-usd",
157
+ rate: 1.087,
158
+ effectiveDate: new Date("2024-01-15T00:00:00.000Z"),
159
+ createdAt: new Date("2024-01-15T00:00:00.000Z"),
160
+ updatedAt: null,
161
+ } as const satisfies ExchangeRate<Schema>;
File without changes
File without changes
File without changes
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createContext } from "./createContext";
3
+
4
+ describe("createContext", () => {
5
+ it("uses permissions array attribute when present", () => {
6
+ const ctx = createContext({
7
+ user: {
8
+ id: "user-1",
9
+ attributes: {
10
+ permissions: ["user-management:createUser", "user-management:activateUser"],
11
+ },
12
+ },
13
+ });
14
+
15
+ expect(ctx.actorId).toBe("user-1");
16
+ expect(ctx.permissions).toEqual(["user-management:createUser", "user-management:activateUser"]);
17
+ });
18
+
19
+ it("returns empty permissions when attributes exists but permissions is absent", () => {
20
+ const ctx = createContext({
21
+ user: {
22
+ id: "user-1",
23
+ attributes: {},
24
+ },
25
+ });
26
+
27
+ expect(ctx.actorId).toBe("user-1");
28
+ expect(ctx.permissions).toEqual([]);
29
+ });
30
+
31
+ it("returns empty permissions when attributes is null", () => {
32
+ const ctx = createContext({
33
+ user: { id: "user-1", attributes: null },
34
+ });
35
+
36
+ expect(ctx.actorId).toBe("user-1");
37
+ expect(ctx.permissions).toEqual([]);
38
+ });
39
+ });
@@ -0,0 +1,15 @@
1
+ import type { CommandContext } from "./types";
2
+
3
+ interface ResolverContext {
4
+ user: {
5
+ id: string;
6
+ attributes: { permissions?: string[] } | null;
7
+ };
8
+ }
9
+
10
+ export function createContext(context: ResolverContext): CommandContext {
11
+ return {
12
+ actorId: context.user.id,
13
+ permissions: [...new Set(context.user.attributes?.permissions ?? [])],
14
+ };
15
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { defineCommand } from "./defineCommand";
3
+ import { InsufficientPermissionError } from "./errors";
4
+ import type { CommandContext } from "./types";
5
+
6
+ describe("defineCommand", () => {
7
+ const ctx: CommandContext = {
8
+ actorId: "user-1",
9
+ permissions: ["mod:doThing"],
10
+ };
11
+
12
+ it("calls impl with db and input when permission is present", async () => {
13
+ const impl = vi.fn().mockResolvedValue({ result: "ok" });
14
+ const command = defineCommand("mod:doThing", impl);
15
+
16
+ const result = await command("fake-db", { foo: "bar" }, ctx);
17
+
18
+ expect(result).toEqual({ result: "ok" });
19
+ expect(impl).toHaveBeenCalledWith("fake-db", { foo: "bar" });
20
+ });
21
+
22
+ it("throws InsufficientPermissionError when permission is missing", async () => {
23
+ const impl = vi.fn();
24
+ const command = defineCommand("mod:doThing", impl);
25
+ const denied: CommandContext = { actorId: "user-1", permissions: [] };
26
+
27
+ await expect(command("fake-db", {}, denied)).rejects.toBeInstanceOf(
28
+ InsufficientPermissionError,
29
+ );
30
+ expect(impl).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("does not pass ctx to impl", async () => {
34
+ const impl = vi.fn().mockResolvedValue({});
35
+ const command = defineCommand("mod:doThing", impl);
36
+
37
+ await command("fake-db", { x: 1 }, ctx);
38
+
39
+ expect(impl).toHaveBeenCalledTimes(1);
40
+ expect(impl.mock.calls[0]).toHaveLength(2);
41
+ });
42
+ });
@@ -0,0 +1,19 @@
1
+ import type { CommandContext } from "./types";
2
+ import { requirePermission } from "./requirePermission";
3
+
4
+ export type Command<TInput, TResult> = (
5
+ db: unknown,
6
+ input: TInput,
7
+ ctx: CommandContext,
8
+ ) => Promise<TResult>;
9
+
10
+ export function defineCommand<TInput, TResult>(
11
+ permission: string,
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ impl: (db: any, input: TInput) => Promise<TResult>,
14
+ ): Command<TInput, TResult> {
15
+ return async (db, input, ctx) => {
16
+ requirePermission(ctx, permission);
17
+ return impl(db, input);
18
+ };
19
+ }
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { definePermissions } from "./definePermissions";
3
+
4
+ describe("definePermissions", () => {
5
+ describe("basic usage (no deps, no aliases)", () => {
6
+ it("creates permissions with moduleName:commandName keys", () => {
7
+ const result = definePermissions("primitives", [
8
+ "createCategory",
9
+ "updateCategory",
10
+ "deleteCategory",
11
+ ] as const);
12
+
13
+ expect(result.permissions.createCategory).toBe("primitives:createCategory");
14
+ expect(result.permissions.updateCategory).toBe("primitives:updateCategory");
15
+ expect(result.permissions.deleteCategory).toBe("primitives:deleteCategory");
16
+ });
17
+
18
+ it("sets own and all to the same values", () => {
19
+ const result = definePermissions("primitives", [
20
+ "createCategory",
21
+ "activateCategory",
22
+ ] as const);
23
+
24
+ expect(result.own).toEqual(["primitives:createCategory", "primitives:activateCategory"]);
25
+ expect(result.all).toEqual(result.own);
26
+ });
27
+
28
+ it("does not include aliases", () => {
29
+ const result = definePermissions("primitives", ["createCategory"] as const);
30
+ expect(result).not.toHaveProperty("aliases");
31
+ });
32
+ });
33
+
34
+ describe("with deps array shorthand", () => {
35
+ it("combines own and deps into all", () => {
36
+ const result = definePermissions("inventory", ["createItem", "updateItem"] as const, [
37
+ "primitives:createCategory",
38
+ "primitives:updateCategory",
39
+ ]);
40
+
41
+ expect(result.own).toEqual(["inventory:createItem", "inventory:updateItem"]);
42
+ expect(result.all).toEqual([
43
+ "inventory:createItem",
44
+ "inventory:updateItem",
45
+ "primitives:createCategory",
46
+ "primitives:updateCategory",
47
+ ]);
48
+ });
49
+
50
+ it("deduplicates when deps overlap with own", () => {
51
+ const result = definePermissions("inventory", ["createItem"] as const, [
52
+ "primitives:createCategory",
53
+ "inventory:createItem",
54
+ ]);
55
+
56
+ expect(result.all).toEqual(["inventory:createItem", "primitives:createCategory"]);
57
+ });
58
+ });
59
+
60
+ describe("with options object", () => {
61
+ it("supports deps without aliases", () => {
62
+ const result = definePermissions("inventory", ["createItem"] as const, {
63
+ deps: ["primitives:createCategory"],
64
+ });
65
+
66
+ expect(result.all).toEqual(["inventory:createItem", "primitives:createCategory"]);
67
+ expect(result).not.toHaveProperty("aliases");
68
+ });
69
+
70
+ it("supports aliases without deps", () => {
71
+ const result = definePermissions(
72
+ "primitives",
73
+ ["createCategory", "activateCategory", "createUnit", "activateUnit"] as const,
74
+ {
75
+ aliases: {
76
+ uom: ["createCategory", "activateCategory"],
77
+ unit: ["createUnit", "activateUnit"],
78
+ },
79
+ },
80
+ );
81
+
82
+ expect(result.aliases.uom).toEqual([
83
+ "primitives:createCategory",
84
+ "primitives:activateCategory",
85
+ ]);
86
+ expect(result.aliases.unit).toEqual(["primitives:createUnit", "primitives:activateUnit"]);
87
+ });
88
+
89
+ it("supports both deps and aliases", () => {
90
+ const result = definePermissions(
91
+ "inventory",
92
+ ["createItem", "updateItem", "viewItem"] as const,
93
+ {
94
+ deps: ["primitives:convertQuantity"],
95
+ aliases: {
96
+ write: ["createItem", "updateItem"],
97
+ read: ["viewItem"],
98
+ },
99
+ },
100
+ );
101
+
102
+ expect(result.all).toContain("primitives:convertQuantity");
103
+ expect(result.aliases.write).toEqual(["inventory:createItem", "inventory:updateItem"]);
104
+ expect(result.aliases.read).toEqual(["inventory:viewItem"]);
105
+ });
106
+ });
107
+
108
+ describe("diamond dependency deduplication", () => {
109
+ it("deduplicates shared transitive deps", () => {
110
+ const primitives = definePermissions("primitives", ["createCategory"] as const);
111
+
112
+ const inventory = definePermissions("inventory", ["createItem"] as const, primitives.all);
113
+
114
+ const product = definePermissions("product", ["createProduct"] as const, primitives.all);
115
+
116
+ // manufacturing depends on both inventory and product
117
+ const manufacturing = definePermissions("manufacturing", ["createWorkOrder"] as const, [
118
+ ...inventory.all,
119
+ ...product.all,
120
+ ]);
121
+
122
+ // primitives:createCategory should appear only once
123
+ const primitivesCount = manufacturing.all.filter(
124
+ (k) => k === "primitives:createCategory",
125
+ ).length;
126
+ expect(primitivesCount).toBe(1);
127
+
128
+ expect(manufacturing.all).toEqual([
129
+ "manufacturing:createWorkOrder",
130
+ "inventory:createItem",
131
+ "primitives:createCategory",
132
+ "product:createProduct",
133
+ ]);
134
+ });
135
+ });
136
+
137
+ describe("type inference", () => {
138
+ it("infers literal types for permission keys", () => {
139
+ const result = definePermissions("primitives", ["createCategory", "updateCategory"] as const);
140
+
141
+ // TypeScript verifies these are literal types at compile time
142
+ const key: "primitives:createCategory" = result.permissions.createCategory;
143
+ expect(key).toBe("primitives:createCategory");
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,94 @@
1
+ type PermissionKey<M extends string, C extends string> = `${M}:${C}`;
2
+
3
+ type PermissionsRecord<M extends string, T extends readonly string[]> = {
4
+ readonly [K in T[number]]: PermissionKey<M, K>;
5
+ };
6
+
7
+ interface PermissionResultBase<M extends string, T extends readonly string[]> {
8
+ permissions: PermissionsRecord<M, T>;
9
+ own: readonly string[];
10
+ all: readonly string[];
11
+ }
12
+
13
+ interface PermissionResultWithAliases<
14
+ M extends string,
15
+ T extends readonly string[],
16
+ A extends Record<string, readonly T[number][]>,
17
+ > extends PermissionResultBase<M, T> {
18
+ aliases: { readonly [K in keyof A]: readonly PermissionKey<M, T[number]>[] };
19
+ }
20
+
21
+ interface DefinePermissionsOptions<T extends readonly string[]> {
22
+ deps?: readonly string[];
23
+ aliases?: Record<string, readonly T[number][]>;
24
+ }
25
+
26
+ // Overload: no deps, no aliases
27
+ export function definePermissions<const M extends string, const T extends readonly string[]>(
28
+ moduleName: M,
29
+ commands: T,
30
+ ): PermissionResultBase<M, T>;
31
+
32
+ // Overload: deps array shorthand
33
+ export function definePermissions<const M extends string, const T extends readonly string[]>(
34
+ moduleName: M,
35
+ commands: T,
36
+ deps: readonly string[],
37
+ ): PermissionResultBase<M, T>;
38
+
39
+ // Overload: options object with aliases
40
+ export function definePermissions<
41
+ const M extends string,
42
+ const T extends readonly string[],
43
+ const A extends Record<string, readonly T[number][]>,
44
+ >(
45
+ moduleName: M,
46
+ commands: T,
47
+ options: { deps?: readonly string[]; aliases: A },
48
+ ): PermissionResultWithAliases<M, T, A>;
49
+
50
+ // Overload: options object without aliases
51
+ export function definePermissions<const M extends string, const T extends readonly string[]>(
52
+ moduleName: M,
53
+ commands: T,
54
+ options: { deps?: readonly string[] },
55
+ ): PermissionResultBase<M, T>;
56
+
57
+ // Implementation
58
+ export function definePermissions(
59
+ moduleName: string,
60
+ commands: readonly string[],
61
+ depsOrOptions?: readonly string[] | DefinePermissionsOptions<readonly string[]>,
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ ): any {
64
+ const isArray = Array.isArray(depsOrOptions);
65
+ const deps: readonly string[] = isArray
66
+ ? depsOrOptions
67
+ : ((depsOrOptions as DefinePermissionsOptions<readonly string[]> | undefined)?.deps ?? []);
68
+ const aliasDefs = !isArray
69
+ ? (depsOrOptions as DefinePermissionsOptions<readonly string[]> | undefined)?.aliases
70
+ : undefined;
71
+
72
+ const permissions: Record<string, string> = {};
73
+ const own: string[] = [];
74
+
75
+ for (const cmd of commands) {
76
+ const key = `${moduleName}:${cmd}`;
77
+ permissions[cmd] = key;
78
+ own.push(key);
79
+ }
80
+
81
+ const all = [...new Set([...own, ...deps])];
82
+
83
+ const result: Record<string, unknown> = { permissions, own, all };
84
+
85
+ if (aliasDefs) {
86
+ const aliases: Record<string, string[]> = {};
87
+ for (const [alias, cmds] of Object.entries(aliasDefs)) {
88
+ aliases[alias] = cmds.map((cmd) => `${moduleName}:${cmd}`);
89
+ }
90
+ result.aliases = aliases;
91
+ }
92
+
93
+ return result;
94
+ }
@@ -0,0 +1,15 @@
1
+ import type { output } from "@tailor-platform/sdk";
2
+ import type { Kysely, Selectable, Insertable, Updateable } from "kysely";
3
+
4
+ export type InferSchema<T> = T extends Kysely<infer S> ? S : never;
5
+ export type { Selectable, Insertable, Updateable };
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/consistent-type-definitions
8
+ export type EmptyFields = {};
9
+
10
+ // TODO: Ideally use Kysely Insertable-equivalent type from SDK instead of inferring from output<T>
11
+ export type FieldsToInsertable<F> = {
12
+ [K in keyof F as null extends output<F[K]> ? never : K]: output<F[K]>;
13
+ } & {
14
+ [K in keyof F as null extends output<F[K]> ? K : never]?: output<F[K]>;
15
+ };
@@ -0,0 +1,22 @@
1
+ export function createDomainError<
2
+ TName extends string,
3
+ TCode extends string,
4
+ TArgs extends unknown[],
5
+ >(name: TName, code: TCode, messageFactory: (...args: TArgs) => string) {
6
+ const ErrorClass = class extends Error {
7
+ override name: TName = name;
8
+ code: TCode = code;
9
+ constructor(...args: TArgs) {
10
+ super(messageFactory(...args));
11
+ }
12
+ };
13
+ Object.defineProperty(ErrorClass, "name", { value: name });
14
+ return ErrorClass;
15
+ }
16
+
17
+ export const InsufficientPermissionError = createDomainError(
18
+ "InsufficientPermissionError",
19
+ "INSUFFICIENT_PERMISSION",
20
+ (actorId: string, requiredPermission: string) =>
21
+ `Actor ${actorId} lacks required permission: ${requiredPermission}`,
22
+ );
@@ -0,0 +1 @@
1
+ export { createContext } from "./createContext";
@@ -0,0 +1,13 @@
1
+ export { defineCommand, type Command } from "./defineCommand";
2
+ export { definePermissions } from "./definePermissions";
3
+ export { requirePermission } from "./requirePermission";
4
+ export { createDomainError, InsufficientPermissionError } from "./errors";
5
+ export type { CommandContext } from "./types";
6
+ export type {
7
+ InferSchema,
8
+ Selectable,
9
+ Insertable,
10
+ Updateable,
11
+ FieldsToInsertable,
12
+ EmptyFields,
13
+ } from "./entityTypes";
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { InsufficientPermissionError } from "./errors";
3
+ import { requirePermission } from "./requirePermission";
4
+ import type { CommandContext } from "./types";
5
+
6
+ describe("requirePermission", () => {
7
+ const ctx: CommandContext = {
8
+ actorId: "user-1",
9
+ permissions: [
10
+ "primitives:createCategory",
11
+ "primitives:activateCategory",
12
+ "inventory:createItem",
13
+ ],
14
+ };
15
+
16
+ it("does not throw when permission is present", () => {
17
+ expect(() => requirePermission(ctx, "primitives:createCategory")).not.toThrow();
18
+ expect(() => requirePermission(ctx, "inventory:createItem")).not.toThrow();
19
+ });
20
+
21
+ it("throws InsufficientPermissionError when permission is missing", () => {
22
+ expect(() => requirePermission(ctx, "primitives:deleteCategory")).toThrow(
23
+ InsufficientPermissionError,
24
+ );
25
+ });
26
+
27
+ it("includes actorId and permission key in error", () => {
28
+ try {
29
+ requirePermission(ctx, "orders:createOrder");
30
+ expect.fail("Should have thrown");
31
+ } catch (err) {
32
+ const error = err as InstanceType<typeof InsufficientPermissionError>;
33
+ expect(error.name).toBe("InsufficientPermissionError");
34
+ expect(error.code).toBe("INSUFFICIENT_PERMISSION");
35
+ expect(error.message).toContain("user-1");
36
+ expect(error.message).toContain("orders:createOrder");
37
+ }
38
+ });
39
+
40
+ it("throws when permissions array is empty", () => {
41
+ const emptyCtx: CommandContext = { actorId: "user-2", permissions: [] };
42
+
43
+ expect(() => requirePermission(emptyCtx, "primitives:createCategory")).toThrow(
44
+ InsufficientPermissionError,
45
+ );
46
+ });
47
+ });
@@ -0,0 +1,8 @@
1
+ import type { CommandContext } from "./types";
2
+ import { InsufficientPermissionError } from "./errors";
3
+
4
+ export function requirePermission(ctx: CommandContext, key: string): void {
5
+ if (!ctx.permissions.includes(key)) {
6
+ throw new InsufficientPermissionError(ctx.actorId, key);
7
+ }
8
+ }
@@ -0,0 +1,4 @@
1
+ export interface CommandContext {
2
+ actorId: string;
3
+ permissions: readonly string[];
4
+ }
File without changes
File without changes