@tailor-platform/erp-kit 0.1.0 → 0.1.2

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 (102) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +77 -50
  4. package/dist/cli.js +693 -553
  5. package/package.json +4 -2
  6. package/schemas/module/command.yml +1 -0
  7. package/schemas/module/model.yml +9 -0
  8. package/schemas/module/query.yml +53 -0
  9. package/skills/1-module-docs/SKILL.md +4 -0
  10. package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
  11. package/skills/2-module-feature-breakdown/SKILL.md +6 -0
  12. package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
  13. package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
  14. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  15. package/skills/3-module-doc-review/SKILL.md +6 -0
  16. package/skills/3-module-doc-review/references/commands.md +54 -0
  17. package/skills/3-module-doc-review/references/models.md +29 -0
  18. package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
  19. package/skills/4-module-tdd-implementation/SKILL.md +24 -6
  20. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  21. package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
  22. package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
  23. package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
  24. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  25. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  26. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  27. package/skills/5-module-implementation-review/SKILL.md +8 -0
  28. package/skills/5-module-implementation-review/references/commands.md +45 -0
  29. package/skills/5-module-implementation-review/references/errors.md +7 -0
  30. package/skills/5-module-implementation-review/references/exports.md +8 -0
  31. package/skills/5-module-implementation-review/references/models.md +30 -0
  32. package/skills/5-module-implementation-review/references/testing.md +29 -0
  33. package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
  34. package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
  35. package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
  36. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
  37. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
  38. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
  39. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  40. package/skills/app-compose-3-doc-review/SKILL.md +4 -0
  41. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  42. package/skills/app-compose-4-design-mock/SKILL.md +8 -0
  43. package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
  44. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  45. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  46. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  47. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  48. package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
  49. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  50. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  51. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  52. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  53. package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
  54. package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
  55. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  56. package/src/cli.ts +8 -90
  57. package/src/commands/app/index.ts +74 -0
  58. package/src/commands/check.test.ts +2 -1
  59. package/src/commands/check.ts +1 -0
  60. package/src/commands/init.test.ts +30 -19
  61. package/src/commands/init.ts +76 -43
  62. package/src/commands/module/index.ts +85 -0
  63. package/src/commands/module/list.test.ts +62 -0
  64. package/src/commands/module/list.ts +64 -0
  65. package/src/commands/scaffold.test.ts +5 -0
  66. package/src/commands/scaffold.ts +2 -1
  67. package/src/commands/sync-check.test.ts +28 -0
  68. package/src/commands/sync-check.ts +6 -0
  69. package/src/integration.test.ts +6 -8
  70. package/src/module.ts +4 -3
  71. package/src/modules/primitives/docs/models/Currency.md +4 -0
  72. package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
  73. package/src/modules/primitives/docs/models/Unit.md +4 -1
  74. package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
  75. package/src/modules/primitives/index.ts +2 -2
  76. package/src/modules/primitives/module.ts +5 -3
  77. package/src/modules/primitives/permissions.ts +0 -2
  78. package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
  79. package/src/modules/primitives/query/convertAmount.ts +122 -0
  80. package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
  81. package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
  82. package/src/modules/shared/defineQuery.test.ts +28 -0
  83. package/src/modules/shared/defineQuery.ts +16 -0
  84. package/src/modules/shared/internal.ts +2 -1
  85. package/src/modules/shared/types.ts +8 -0
  86. package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
  87. package/src/modules/user-management/docs/models/Permission.md +2 -0
  88. package/src/modules/user-management/docs/models/Role.md +2 -0
  89. package/src/modules/user-management/docs/models/RolePermission.md +2 -0
  90. package/src/modules/user-management/docs/models/User.md +2 -0
  91. package/src/modules/user-management/docs/models/UserRole.md +2 -0
  92. package/src/schemas.ts +1 -0
  93. package/rules/app-compose/frontend/auth.md +0 -55
  94. package/rules/app-compose/frontend/page.md +0 -86
  95. package/rules/module-development/cross-module-type-injection.md +0 -28
  96. package/rules/module-development/dependency-modules.md +0 -24
  97. package/rules/module-development/executors.md +0 -67
  98. package/rules/module-development/sync-vs-async-operations.md +0 -83
  99. package/rules/sdk-best-practices/sdk-docs.md +0 -14
  100. package/src/modules/primitives/command/convertAmount.ts +0 -126
  101. /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
  102. /package/src/modules/primitives/docs/{commands → queries}/ConvertQuantity.md +0 -0
@@ -9,11 +9,11 @@ import { deactivateCategory } from "./command/deactivateCategory";
9
9
  import { activateUnit } from "./command/activateUnit";
10
10
  import { deactivateUnit } from "./command/deactivateUnit";
11
11
  import { setReferenceUnit } from "./command/setReferenceUnit";
12
- import { convertQuantity } from "./command/convertQuantity";
12
+ import { convertQuantity } from "./query/convertQuantity";
13
13
  import { activateCurrency } from "./command/activateCurrency";
14
14
  import { deactivateCurrency } from "./command/deactivateCurrency";
15
15
  import { setBaseCurrency } from "./command/setBaseCurrency";
16
- import { convertAmount } from "./command/convertAmount";
16
+ import { convertAmount } from "./query/convertAmount";
17
17
  import { createUoMCategoryType, CreateUoMCategoryTypeParams } from "./db/uomCategory";
18
18
  import { createUnitType, CreateUnitTypeParams } from "./db/unit";
19
19
  import { createCurrencyType, CreateCurrencyTypeParams } from "./db/currency";
@@ -56,10 +56,12 @@ export const defineModule = <
56
56
  activateUnit,
57
57
  deactivateUnit,
58
58
  setReferenceUnit,
59
- convertQuantity,
60
59
  activateCurrency,
61
60
  deactivateCurrency,
62
61
  setBaseCurrency,
62
+ },
63
+ queries: {
64
+ convertQuantity,
63
65
  convertAmount,
64
66
  },
65
67
  };
@@ -1,7 +1,6 @@
1
1
  import { definePermissions } from "../shared/internal";
2
2
 
3
3
  export const { permissions, own, all } = definePermissions("primitives", [
4
- "convertQuantity",
5
4
  "createCategory",
6
5
  "activateCategory",
7
6
  "deactivateCategory",
@@ -9,7 +8,6 @@ export const { permissions, own, all } = definePermissions("primitives", [
9
8
  "createUnit",
10
9
  "activateUnit",
11
10
  "deactivateUnit",
12
- "convertAmount",
13
11
  "createCurrency",
14
12
  "activateCurrency",
15
13
  "deactivateCurrency",
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { InsufficientPermissionError, type CommandContext } from "../../shared/internal";
3
2
  import { createMockDb } from "../../testing/index";
4
3
  import { DB } from "../generated/kysely-tailordb";
4
+ import type { QueryContext } from "../../shared/internal";
5
5
  import {
6
6
  CurrencyNotFoundError,
7
7
  ExchangeRateNotFoundError,
@@ -19,7 +19,7 @@ import {
19
19
  import { convertAmount } from "./convertAmount";
20
20
 
21
21
  describe("convertAmount", () => {
22
- const ctx: CommandContext = { actorId: "test-actor", permissions: ["primitives:convertAmount"] };
22
+ const ctx: QueryContext = { actorId: "test-actor" };
23
23
 
24
24
  // Error cases first
25
25
  it("throws when source currency doesn't exist", async () => {
@@ -255,21 +255,4 @@ describe("convertAmount", () => {
255
255
 
256
256
  expect(result.convertedAmount).toBe(0);
257
257
  });
258
-
259
- it("throws when permission is missing", async () => {
260
- const { db } = createMockDb<DB>();
261
- const denied: CommandContext = { actorId: "test-actor", permissions: [] };
262
- await expect(
263
- convertAmount(
264
- db,
265
- {
266
- amount: 100,
267
- sourceCurrencyCode: "USD",
268
- targetCurrencyCode: "EUR",
269
- conversionDate: "2024-01-15",
270
- },
271
- denied,
272
- ),
273
- ).rejects.toBeInstanceOf(InsufficientPermissionError);
274
- });
275
258
  });
@@ -0,0 +1,122 @@
1
+ import { defineQuery, type ReadonlyDB } from "../../shared/internal";
2
+ import type { DB } from "../generated/kysely-tailordb";
3
+ import {
4
+ CurrencyNotFoundError,
5
+ ExchangeRateNotFoundError,
6
+ InactiveCurrencyError,
7
+ } from "../lib/errors";
8
+
9
+ export interface ConvertAmountInput {
10
+ amount: number;
11
+ sourceCurrencyCode: string;
12
+ targetCurrencyCode: string;
13
+ conversionDate: string;
14
+ }
15
+
16
+ /**
17
+ * Function: convertAmount
18
+ *
19
+ * Converts a monetary amount from one currency to another using the applicable
20
+ * exchange rate for a given date. Currencies are identified by their ISO 4217
21
+ * code (e.g., "USD", "EUR", "JPY"). The function looks up the most recent exchange
22
+ * rate on or before the specified date. If no direct rate exists, it calculates
23
+ * the inverse rate. Result is rounded to the target currency's decimal precision.
24
+ */
25
+ export const convertAmount = defineQuery(async (db: ReadonlyDB<DB>, input: ConvertAmountInput) => {
26
+ // 1. Validate source currency exists
27
+ const sourceCurrency = await db
28
+ .selectFrom("Currency")
29
+ .selectAll()
30
+ .where("code", "=", input.sourceCurrencyCode)
31
+ .executeTakeFirst();
32
+
33
+ if (!sourceCurrency) {
34
+ throw new CurrencyNotFoundError(input.sourceCurrencyCode);
35
+ }
36
+
37
+ // 2. Validate target currency exists
38
+ const targetCurrency = await db
39
+ .selectFrom("Currency")
40
+ .selectAll()
41
+ .where("code", "=", input.targetCurrencyCode)
42
+ .executeTakeFirst();
43
+
44
+ if (!targetCurrency) {
45
+ throw new CurrencyNotFoundError(input.targetCurrencyCode);
46
+ }
47
+
48
+ // 3. Validate both currencies are active
49
+ if (!sourceCurrency.isActive) {
50
+ throw new InactiveCurrencyError(input.sourceCurrencyCode);
51
+ }
52
+
53
+ if (!targetCurrency.isActive) {
54
+ throw new InactiveCurrencyError(input.targetCurrencyCode);
55
+ }
56
+
57
+ // 4. Same currency - return original amount
58
+ if (sourceCurrency.id === targetCurrency.id) {
59
+ return {
60
+ convertedAmount: input.amount,
61
+ exchangeRate: 1,
62
+ sourceCurrency,
63
+ targetCurrency,
64
+ exchangeRateRecord: null,
65
+ };
66
+ }
67
+
68
+ // 5. Find direct exchange rate (most recent on or before conversion date)
69
+ const conversionDate = new Date(input.conversionDate);
70
+ const directRate = await db
71
+ .selectFrom("ExchangeRate")
72
+ .selectAll()
73
+ .where("sourceCurrencyId", "=", sourceCurrency.id)
74
+ .where("targetCurrencyId", "=", targetCurrency.id)
75
+ .where("effectiveDate", "<=", conversionDate)
76
+ .orderBy("effectiveDate", "desc")
77
+ .executeTakeFirst();
78
+
79
+ let exchangeRate: number;
80
+ let exchangeRateRecord = null;
81
+
82
+ if (directRate) {
83
+ exchangeRate = directRate.rate;
84
+ exchangeRateRecord = directRate;
85
+ } else {
86
+ // 6. Try inverse rate
87
+ const inverseRate = await db
88
+ .selectFrom("ExchangeRate")
89
+ .selectAll()
90
+ .where("sourceCurrencyId", "=", targetCurrency.id)
91
+ .where("targetCurrencyId", "=", sourceCurrency.id)
92
+ .where("effectiveDate", "<=", conversionDate)
93
+ .orderBy("effectiveDate", "desc")
94
+ .executeTakeFirst();
95
+
96
+ if (!inverseRate) {
97
+ throw new ExchangeRateNotFoundError(
98
+ input.sourceCurrencyCode,
99
+ input.targetCurrencyCode,
100
+ input.conversionDate,
101
+ );
102
+ }
103
+
104
+ exchangeRate = 1 / inverseRate.rate;
105
+ exchangeRateRecord = inverseRate;
106
+ }
107
+
108
+ // 7. Calculate converted amount
109
+ const rawResult = input.amount * exchangeRate;
110
+
111
+ // 8. Round to target currency's decimal places
112
+ const roundingFactor = Math.pow(10, targetCurrency.decimalPlaces);
113
+ const convertedAmount = Math.round(rawResult * roundingFactor) / roundingFactor;
114
+
115
+ return {
116
+ convertedAmount,
117
+ exchangeRate,
118
+ sourceCurrency,
119
+ targetCurrency,
120
+ exchangeRateRecord,
121
+ };
122
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { createMockDb } from "../../testing/index";
3
- import { InsufficientPermissionError, type CommandContext } from "../../shared/internal";
4
3
  import { DB } from "../generated/kysely-tailordb";
4
+ import type { QueryContext } from "../../shared/internal";
5
5
  import { InactiveUnitError, IncompatibleUnitsError, UnitNotFoundError } from "../lib/errors";
6
6
  import {
7
7
  baseUnitGram,
@@ -13,10 +13,7 @@ import {
13
13
  import { convertQuantity } from "./convertQuantity";
14
14
 
15
15
  describe("convertQuantity", () => {
16
- const ctx: CommandContext = {
17
- actorId: "test-actor",
18
- permissions: ["primitives:convertQuantity"],
19
- };
16
+ const ctx: QueryContext = { actorId: "test-actor" };
20
17
 
21
18
  // Error cases first
22
19
  it("throws when source unit doesn't exist", async () => {
@@ -208,12 +205,4 @@ describe("convertQuantity", () => {
208
205
 
209
206
  expect(result.convertedQuantity).toBe(0);
210
207
  });
211
-
212
- it("throws when permission is missing", async () => {
213
- const { db } = createMockDb<DB>();
214
- const denied: CommandContext = { actorId: "test-actor", permissions: [] };
215
- await expect(
216
- convertQuantity(db, { quantity: 1, sourceUnitSymbol: "kg", targetUnitSymbol: "g" }, denied),
217
- ).rejects.toBeInstanceOf(InsufficientPermissionError);
218
- });
219
208
  });
@@ -1,7 +1,6 @@
1
- import { defineCommand } from "../../shared/internal";
2
- import { DB } from "../generated/kysely-tailordb";
1
+ import { defineQuery, type ReadonlyDB } from "../../shared/internal";
2
+ import type { DB } from "../generated/kysely-tailordb";
3
3
  import { InactiveUnitError, IncompatibleUnitsError, UnitNotFoundError } from "../lib/errors";
4
- import { permissions } from "../permissions";
5
4
 
6
5
  export interface ConvertQuantityInput {
7
6
  quantity: number;
@@ -17,9 +16,8 @@ export interface ConvertQuantityInput {
17
16
  * The conversion uses each unit's conversion factor relative to the category's reference unit.
18
17
  * Result is rounded to the target unit's precision setting.
19
18
  */
20
- export const convertQuantity = defineCommand(
21
- permissions.convertQuantity,
22
- async (db: DB, input: ConvertQuantityInput) => {
19
+ export const convertQuantity = defineQuery(
20
+ async (db: ReadonlyDB<DB>, input: ConvertQuantityInput) => {
23
21
  // 1. Validate source unit exists
24
22
  const sourceUnit = await db
25
23
  .selectFrom("Unit")
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { defineQuery } from "./defineQuery";
3
+ import type { QueryContext } from "./types";
4
+
5
+ describe("defineQuery", () => {
6
+ const ctx: QueryContext = { actorId: "user-1" };
7
+
8
+ it("calls impl with db, input, and ctx", async () => {
9
+ const impl = vi.fn().mockResolvedValue({ result: "ok" });
10
+ const query = defineQuery(impl);
11
+
12
+ const result = await query("fake-db", { foo: "bar" }, ctx);
13
+
14
+ expect(result).toEqual({ result: "ok" });
15
+ expect(impl).toHaveBeenCalledWith("fake-db", { foo: "bar" }, ctx);
16
+ });
17
+
18
+ it("passes ctx with actorId to impl", async () => {
19
+ const impl = vi.fn().mockResolvedValue({});
20
+ const query = defineQuery(impl);
21
+
22
+ await query("fake-db", { x: 1 }, ctx);
23
+
24
+ expect(impl).toHaveBeenCalledTimes(1);
25
+ expect(impl.mock.calls[0]).toHaveLength(3);
26
+ expect(impl.mock.calls[0][2]).toEqual({ actorId: "user-1" });
27
+ });
28
+ });
@@ -0,0 +1,16 @@
1
+ import type { QueryContext } from "./types";
2
+
3
+ export type Query<TInput, TResult> = (
4
+ db: unknown,
5
+ input: TInput,
6
+ ctx: QueryContext,
7
+ ) => Promise<TResult>;
8
+
9
+ export function defineQuery<TInput, TResult>(
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ impl: (db: any, input: TInput, ctx: QueryContext) => Promise<TResult>,
12
+ ): Query<TInput, TResult> {
13
+ return async (db, input, ctx) => {
14
+ return impl(db, input, ctx);
15
+ };
16
+ }
@@ -1,8 +1,9 @@
1
1
  export { defineCommand, type Command } from "./defineCommand";
2
+ export { defineQuery, type Query } from "./defineQuery";
2
3
  export { definePermissions } from "./definePermissions";
3
4
  export { requirePermission } from "./requirePermission";
4
5
  export { createDomainError, InsufficientPermissionError } from "./errors";
5
- export type { CommandContext } from "./types";
6
+ export type { CommandContext, QueryContext, ReadonlyDB } from "./types";
6
7
  export type {
7
8
  InferSchema,
8
9
  Selectable,
@@ -1,4 +1,12 @@
1
+ export interface QueryContext {
2
+ actorId: string;
3
+ }
4
+
1
5
  export interface CommandContext {
2
6
  actorId: string;
3
7
  permissions: readonly string[];
4
8
  }
9
+
10
+ // Strips write methods (updateTable, insertInto, deleteFrom) from a Kysely DB instance.
11
+ // Query handlers use this instead of the full DB type to get compile-time write prevention.
12
+ export type ReadonlyDB<T extends { selectFrom: unknown }> = Pick<T, "selectFrom">;
@@ -16,6 +16,8 @@ AppendOnly
16
16
 
17
17
  - [logAuditEvent](../commands/LogAuditEvent.md)
18
18
 
19
+ ### Query Definitions
20
+
19
21
  ### Models
20
22
 
21
23
  - AuditEvent
@@ -16,6 +16,8 @@ Standard
16
16
 
17
17
  - [createPermission](../commands/CreatePermission.md)
18
18
 
19
+ ### Query Definitions
20
+
19
21
  ### Models
20
22
 
21
23
  - Permission
@@ -16,6 +16,8 @@ Standard
16
16
 
17
17
  - [createRole](../commands/CreateRole.md)
18
18
 
19
+ ### Query Definitions
20
+
19
21
  ### Models
20
22
 
21
23
  - Role
@@ -17,6 +17,8 @@ Standard
17
17
  - [assignPermissionToRole](../commands/AssignPermissionToRole.md)
18
18
  - [revokePermissionFromRole](../commands/RevokePermissionFromRole.md)
19
19
 
20
+ ### Query Definitions
21
+
20
22
  ### Models
21
23
 
22
24
  - RolePermission
@@ -29,6 +29,8 @@ stateDiagram-v2
29
29
  - [deactivateUser](../commands/DeactivateUser.md)
30
30
  - [reactivateUser](../commands/ReactivateUser.md)
31
31
 
32
+ ### Query Definitions
33
+
32
34
  ### Models
33
35
 
34
36
  - User
@@ -17,6 +17,8 @@ Standard
17
17
  - [assignRoleToUser](../commands/AssignRoleToUser.md)
18
18
  - [revokeRoleFromUser](../commands/RevokeRoleFromUser.md)
19
19
 
20
+ ### Query Definitions
21
+
20
22
  ### Models
21
23
 
22
24
  - UserRole
package/src/schemas.ts CHANGED
@@ -8,6 +8,7 @@ export const MODULE_SCHEMAS = {
8
8
  command: path.join(SCHEMAS_ROOT, "module", "command.yml"),
9
9
  model: path.join(SCHEMAS_ROOT, "module", "model.yml"),
10
10
  feature: path.join(SCHEMAS_ROOT, "module", "feature.yml"),
11
+ query: path.join(SCHEMAS_ROOT, "module", "query.yml"),
11
12
  } as const;
12
13
 
13
14
  export const APP_COMPOSE_SCHEMAS = {
@@ -1,55 +0,0 @@
1
- ---
2
- paths:
3
- - "examples/**/frontend/src/App.tsx"
4
- - "examples/**/frontend/src/lib/auth-client.ts"
5
- - "examples/**/frontend/src/providers/graphql-provider.tsx"
6
- ---
7
-
8
- # Frontend Auth (AppShell AuthProvider)
9
-
10
- Use `@tailor-platform/app-shell` authentication as the default pattern for frontend apps.
11
-
12
- ## Required Environment Variables
13
-
14
- - `VITE_TAILOR_APP_URL`
15
- - `VITE_TAILOR_CLIENT_ID`
16
-
17
- Fail fast at startup if either value is missing.
18
-
19
- ## Auth Client Initialization
20
-
21
- Create a single shared auth client in `src/lib/auth-client.ts`:
22
-
23
- - Use `createAuthClient({ appUri, clientId })`
24
- - Export it as `authClient`
25
- - Do not create auth clients inside render functions
26
-
27
- ## App Root Composition
28
-
29
- Wrap the app with `AuthProvider` in `src/App.tsx`:
30
-
31
- - `AuthProvider` must receive the shared `authClient`
32
- - Use `guardComponent` for unauthenticated/loading states
33
- - `guardComponent` should use `useAuth()` and handle:
34
- - `!isReady`: loading UI
35
- - `!isAuthenticated`: login UI with `login()`
36
- - `error`: visible message
37
-
38
- Recommended order:
39
-
40
- 1. `AuthProvider`
41
- 2. `GraphQLProvider`
42
- 3. `AppShell`
43
-
44
- ## GraphQL Authentication
45
-
46
- `src/providers/graphql-provider.tsx` must attach OAuth headers for every request:
47
-
48
- - Accept `authClient` as a prop
49
- - Call `authClient.getAuthHeadersForQuery()`
50
- - Set both headers:
51
- - `Authorization`
52
- - `DPoP`
53
- - Keep `Content-Type: application/json`
54
-
55
- Do not use unauthenticated static headers for `/query`.
@@ -1,86 +0,0 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/page.tsx"
4
- ---
5
-
6
- # Page File Structure (File-Based Routing)
7
-
8
- Each `page.tsx` file should contain a default-exported page component with optional `appShellPageProps` static field.
9
- Pages are automatically discovered by the Vite plugin.
10
-
11
- ## Path Convention
12
-
13
- The URL path is derived from the directory structure:
14
-
15
- ```
16
- src/pages/
17
- ├── page.tsx # / (root path)
18
- ├── purchasing/
19
- │ ├── page.tsx # /purchasing
20
- │ └── orders/
21
- │ ├── page.tsx # /purchasing/orders
22
- │ └── [id]/
23
- │ └── page.tsx # /purchasing/orders/:id
24
- └── (admin)/ # Grouping (not included in path)
25
- └── settings/
26
- └── page.tsx # /settings
27
- ```
28
-
29
- | Directory Name | Converts To | Description |
30
- | -------------- | ----------- | ----------------------------- |
31
- | `orders` | `orders` | Static segment |
32
- | `[id]` | `:id` | Dynamic parameter |
33
- | `(group)` | (excluded) | Grouping only (not in path) |
34
- | `_lib` | (ignored) | Not routed (for shared logic) |
35
-
36
- ## Page Component Pattern
37
-
38
- ```tsx
39
- import { Layout, type AppShellPageProps } from "@tailor-platform/app-shell";
40
- import { useQuery } from "urql";
41
- import { graphql } from "@/graphql";
42
- import { ErrorFallback } from "@/components/composed/error-fallback";
43
- import { Loading } from "@/components/composed/loading";
44
-
45
- const MyQuery = graphql(`...`);
46
-
47
- const MyPage = () => {
48
- const [{ data, error, fetching }, reexecuteQuery] = useQuery({
49
- query: MyQuery,
50
- });
51
-
52
- if (fetching) return <Loading />;
53
-
54
- if (error || !data) {
55
- return (
56
- <ErrorFallback
57
- title="Failed to load"
58
- message="An error occurred while fetching data."
59
- onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
60
- />
61
- );
62
- }
63
-
64
- return (
65
- <Layout columns={1} title="My Page">
66
- <Layout.Column>
67
- <MyComponent data={data} />
68
- </Layout.Column>
69
- </Layout>
70
- );
71
- };
72
-
73
- MyPage.appShellPageProps = {
74
- meta: { title: "My Page" },
75
- } satisfies AppShellPageProps;
76
-
77
- export default MyPage;
78
- ```
79
-
80
- ## Key Points
81
-
82
- - Handle `fetching` state with `<Loading />`
83
- - Handle `error || !data` with `<ErrorFallback />` and `reexecuteQuery` for retry
84
- - Use `appShellPageProps` static field for metadata (title, icon) and guards
85
- - Guards on parent pages are automatically inherited by child pages
86
- - See `component.md` for fragment collocation
@@ -1,28 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/db/*.ts"
4
- - "modules/*/src/module.ts"
5
- ---
6
-
7
- # Cross-Module Type Injection
8
-
9
- ## Typing External Module References
10
-
11
- - Derive types from the source module's `defineModule` return type, never use `any`
12
- - Use `import type` for type-only imports
13
- - All modules live in the same package (`@tailor-platform/erp-kit`), so use relative paths
14
- - Define a local type alias for readability: `type UnitType = ReturnType<typeof definePrimitivesModule>["unit"]`
15
-
16
- ## DB Type Creator Pattern (`src/db/*.ts`)
17
-
18
- - Accept optional external type via `Create*TypeParams` (e.g., `unitType?: UnitType`)
19
- - Use a ternary on the param to conditionally add `.relation()`:
20
- - **Present**: `db.uuid().relation({ type: "n-1", toward: { type: param }, backward: "..." })`
21
- - **Absent**: `db.uuid()` (plain UUID, no relation) — preserves standalone usage
22
- - Export a default instance at file bottom with `{}` params for internal/standalone use
23
-
24
- ## Module Wiring (`src/module.ts`)
25
-
26
- - Group external dependencies under a `primitives?` key in `DefineModuleParams`
27
- - Spread consumer's `Create*TypeParams` and merge the injected type: `{ ...params.productTemplate, unitType: params.primitives?.unit }`
28
- - Export `DefineModuleParams` type from `index.ts` so consumers can type their config
@@ -1,24 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/"
4
- ---
5
-
6
- # Dependency Modules
7
-
8
- When a module depends on another module, create `modules/<module-name>/src/dep.ts` to instantiate and re-export the dependency:
9
-
10
- ```typescript
11
- import { defineModule } from "../../primitives/src/module";
12
-
13
- const primitives = defineModule();
14
-
15
- // Re-export db types for use in this module's db definitions and commands
16
- export const { unit, currency } = primitives.db;
17
- ```
18
-
19
- ## Module Return Structure
20
-
21
- `defineModule` returns an object with:
22
-
23
- - `db`: Object of database model types (keyed by name)
24
- - `executors`: Object of executor instances (keyed by name, if any)
@@ -1,67 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/executor/"
4
- - "modules/*/src/module.ts"
5
- ---
6
-
7
- # Executors
8
-
9
- Executors handle asynchronous operations triggered by database record changes.
10
-
11
- ## Factory Pattern
12
-
13
- Executors are **factory functions** that accept configuration and return an executor:
14
-
15
- ```typescript
16
- export const myExecutor = function myExecutor({ namespace }: { namespace: string }) {
17
- return createExecutor({
18
- name: "myExecutor",
19
- // ... executor config
20
- });
21
- };
22
- ```
23
-
24
- **Why factory functions:**
25
-
26
- - Executors need runtime configuration (db namespace) not known at import time
27
- - Named function expression enables better stack traces
28
- - Module consumers control configuration via `defineModule` params
29
-
30
- ## File Organization
31
-
32
- - Place in `src/executor/` directory
33
- - Group related executors in one file (e.g., `recomputeOnRolePermissionChange.ts`)
34
- - Name files after the operation, not the trigger
35
-
36
- ## Executor Structure
37
-
38
- Required fields:
39
-
40
- - `name`: Matches function name exactly
41
- - `description`: Human-readable purpose
42
- - `trigger`: `recordCreatedTrigger` or `recordDeletedTrigger` with `type`
43
- - `operation`: `kind: "jobFunction"` with async `body`
44
-
45
- ## Database Access
46
-
47
- Use namespace parameter with `getDB`:
48
-
49
- ```typescript
50
- // @ts-expect-error unsure at build time
51
- const db = getDB(namespace);
52
- ```
53
-
54
- The `@ts-expect-error` comment is required because the namespace is validated at runtime.
55
-
56
- ## Module Integration
57
-
58
- `defineModule` returns executors in a dedicated array:
59
-
60
- ```typescript
61
- return {
62
- db: [user, role, ...],
63
- executors: [rolePermissionCreated, rolePermissionDeleted],
64
- };
65
- ```
66
-
67
- Instantiate executors in `module.ts` with the `dbNamespace` parameter.