@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +77 -50
- package/dist/cli.js +693 -553
- package/package.json +4 -2
- package/schemas/module/command.yml +1 -0
- package/schemas/module/model.yml +9 -0
- package/schemas/module/query.yml +53 -0
- package/skills/1-module-docs/SKILL.md +4 -0
- package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
- package/skills/2-module-feature-breakdown/SKILL.md +6 -0
- package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
- package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
- package/skills/2-module-feature-breakdown/references/structure.md +22 -0
- package/skills/3-module-doc-review/SKILL.md +6 -0
- package/skills/3-module-doc-review/references/commands.md +54 -0
- package/skills/3-module-doc-review/references/models.md +29 -0
- package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
- package/skills/4-module-tdd-implementation/SKILL.md +24 -6
- package/skills/4-module-tdd-implementation/references/commands.md +45 -0
- package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
- package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
- package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
- package/skills/4-module-tdd-implementation/references/models.md +30 -0
- package/skills/4-module-tdd-implementation/references/structure.md +22 -0
- package/skills/4-module-tdd-implementation/references/testing.md +37 -0
- package/skills/5-module-implementation-review/SKILL.md +8 -0
- package/skills/5-module-implementation-review/references/commands.md +45 -0
- package/skills/5-module-implementation-review/references/errors.md +7 -0
- package/skills/5-module-implementation-review/references/exports.md +8 -0
- package/skills/5-module-implementation-review/references/models.md +30 -0
- package/skills/5-module-implementation-review/references/testing.md +29 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
- package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
- package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
- package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
- package/skills/app-compose-3-doc-review/SKILL.md +4 -0
- package/skills/app-compose-3-doc-review/references/structure.md +27 -0
- package/skills/app-compose-4-design-mock/SKILL.md +8 -0
- package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
- package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
- package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
- package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
- package/skills/app-compose-4-design-mock/references/structure.md +27 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
- package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
- package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
- package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
- package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
- package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
- package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
- package/src/cli.ts +8 -90
- package/src/commands/app/index.ts +74 -0
- package/src/commands/check.test.ts +2 -1
- package/src/commands/check.ts +1 -0
- package/src/commands/init.test.ts +30 -19
- package/src/commands/init.ts +76 -43
- package/src/commands/module/index.ts +85 -0
- package/src/commands/module/list.test.ts +62 -0
- package/src/commands/module/list.ts +64 -0
- package/src/commands/scaffold.test.ts +5 -0
- package/src/commands/scaffold.ts +2 -1
- package/src/commands/sync-check.test.ts +28 -0
- package/src/commands/sync-check.ts +6 -0
- package/src/integration.test.ts +6 -8
- package/src/module.ts +4 -3
- package/src/modules/primitives/docs/models/Currency.md +4 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
- package/src/modules/primitives/docs/models/Unit.md +4 -1
- package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
- package/src/modules/primitives/index.ts +2 -2
- package/src/modules/primitives/module.ts +5 -3
- package/src/modules/primitives/permissions.ts +0 -2
- package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
- package/src/modules/primitives/query/convertAmount.ts +122 -0
- package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
- package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
- package/src/modules/shared/defineQuery.test.ts +28 -0
- package/src/modules/shared/defineQuery.ts +16 -0
- package/src/modules/shared/internal.ts +2 -1
- package/src/modules/shared/types.ts +8 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
- package/src/modules/user-management/docs/models/Permission.md +2 -0
- package/src/modules/user-management/docs/models/Role.md +2 -0
- package/src/modules/user-management/docs/models/RolePermission.md +2 -0
- package/src/modules/user-management/docs/models/User.md +2 -0
- package/src/modules/user-management/docs/models/UserRole.md +2 -0
- package/src/schemas.ts +1 -0
- package/rules/app-compose/frontend/auth.md +0 -55
- package/rules/app-compose/frontend/page.md +0 -86
- package/rules/module-development/cross-module-type-injection.md +0 -28
- package/rules/module-development/dependency-modules.md +0 -24
- package/rules/module-development/executors.md +0 -67
- package/rules/module-development/sync-vs-async-operations.md +0 -83
- package/rules/sdk-best-practices/sdk-docs.md +0 -14
- package/src/modules/primitives/command/convertAmount.ts +0 -126
- /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
- /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 "./
|
|
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 "./
|
|
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:
|
|
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:
|
|
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 {
|
|
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 =
|
|
21
|
-
|
|
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">;
|
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.
|