@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.
- package/README.md +196 -28
- package/dist/cli.js +894 -0
- package/package.json +65 -8
- package/rules/app-compose/backend/auth.md +78 -0
- package/rules/app-compose/frontend/auth.md +55 -0
- package/rules/app-compose/frontend/component.md +55 -0
- package/rules/app-compose/frontend/page.md +86 -0
- package/rules/app-compose/frontend/screen-detailview.md +112 -0
- package/rules/app-compose/frontend/screen-form.md +145 -0
- package/rules/app-compose/frontend/screen-listview.md +159 -0
- package/rules/app-compose/structure.md +32 -0
- package/rules/module-development/commands.md +54 -0
- package/rules/module-development/cross-module-type-injection.md +28 -0
- package/rules/module-development/dependency-modules.md +24 -0
- package/rules/module-development/errors.md +12 -0
- package/rules/module-development/executors.md +67 -0
- package/rules/module-development/exports.md +13 -0
- package/rules/module-development/models.md +34 -0
- package/rules/module-development/structure.md +27 -0
- package/rules/module-development/sync-vs-async-operations.md +83 -0
- package/rules/module-development/testing.md +43 -0
- package/rules/sdk-best-practices/db-relations.md +74 -0
- package/rules/sdk-best-practices/sdk-docs.md +14 -0
- package/schemas/app-compose/actors.yml +34 -0
- package/schemas/app-compose/business-flow.yml +50 -0
- package/schemas/app-compose/requirements.yml +33 -0
- package/schemas/app-compose/resolver.yml +47 -0
- package/schemas/app-compose/screen.yml +81 -0
- package/schemas/app-compose/story.yml +67 -0
- package/schemas/module/command.yml +52 -0
- package/schemas/module/feature.yml +58 -0
- package/schemas/module/model.yml +70 -0
- package/schemas/module/module.yml +50 -0
- package/skills/1-module-docs/SKILL.md +107 -0
- package/skills/2-module-feature-breakdown/SKILL.md +66 -0
- package/skills/3-module-doc-review/SKILL.md +230 -0
- package/skills/4-module-tdd-implementation/SKILL.md +56 -0
- package/skills/5-module-implementation-review/SKILL.md +400 -0
- package/skills/app-compose-1-requirement-analysis/SKILL.md +85 -0
- package/skills/app-compose-2-requirements-breakdown/SKILL.md +88 -0
- package/skills/app-compose-3-doc-review/SKILL.md +112 -0
- package/skills/app-compose-4-design-mock/SKILL.md +248 -0
- package/skills/app-compose-5-design-mock-review/SKILL.md +283 -0
- package/skills/app-compose-6-implementation-spec/SKILL.md +122 -0
- package/skills/mock-scenario/SKILL.md +118 -0
- package/src/app.ts +1 -0
- package/src/cli.ts +120 -0
- package/src/commands/check.test.ts +30 -0
- package/src/commands/check.ts +66 -0
- package/src/commands/init.test.ts +77 -0
- package/src/commands/init.ts +87 -0
- package/src/commands/mock/index.ts +53 -0
- package/src/commands/mock/start.ts +179 -0
- package/src/commands/mock/validate.test.ts +185 -0
- package/src/commands/mock/validate.ts +198 -0
- package/src/commands/scaffold.test.ts +76 -0
- package/src/commands/scaffold.ts +119 -0
- package/src/commands/sync-check.test.ts +125 -0
- package/src/commands/sync-check.ts +182 -0
- package/src/integration.test.ts +63 -0
- package/src/mdschema.ts +48 -0
- package/src/mockServer.ts +55 -0
- package/src/module.ts +86 -0
- package/src/modules/accounting/.gitkeep +0 -0
- package/src/modules/coa-management/.gitkeep +0 -0
- package/src/modules/inventory/.gitkeep +0 -0
- package/src/modules/manufacturing/.gitkeep +0 -0
- package/src/modules/primitives/README.md +39 -0
- package/src/modules/primitives/command/activateCategory.test.ts +75 -0
- package/src/modules/primitives/command/activateCategory.ts +50 -0
- package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
- package/src/modules/primitives/command/activateCurrency.ts +50 -0
- package/src/modules/primitives/command/activateUnit.test.ts +53 -0
- package/src/modules/primitives/command/activateUnit.ts +50 -0
- package/src/modules/primitives/command/convertAmount.test.ts +275 -0
- package/src/modules/primitives/command/convertAmount.ts +126 -0
- package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
- package/src/modules/primitives/command/convertQuantity.ts +73 -0
- package/src/modules/primitives/command/createCategory.test.ts +126 -0
- package/src/modules/primitives/command/createCategory.ts +89 -0
- package/src/modules/primitives/command/createCurrency.test.ts +191 -0
- package/src/modules/primitives/command/createCurrency.ts +77 -0
- package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
- package/src/modules/primitives/command/createExchangeRate.ts +91 -0
- package/src/modules/primitives/command/createUnit.test.ts +214 -0
- package/src/modules/primitives/command/createUnit.ts +88 -0
- package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
- package/src/modules/primitives/command/deactivateCategory.ts +62 -0
- package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
- package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
- package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
- package/src/modules/primitives/command/deactivateUnit.ts +62 -0
- package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
- package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
- package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
- package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
- package/src/modules/primitives/db/currency.ts +30 -0
- package/src/modules/primitives/db/exchangeRate.ts +28 -0
- package/src/modules/primitives/db/unit.ts +32 -0
- package/src/modules/primitives/db/uomCategory.ts +32 -0
- package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
- package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
- package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
- package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
- package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
- package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
- package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
- package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
- package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
- package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
- package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
- package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
- package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
- package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
- package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
- package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
- package/src/modules/primitives/docs/features/uom-categories.md +52 -0
- package/src/modules/primitives/docs/models/Currency.md +45 -0
- package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
- package/src/modules/primitives/docs/models/Unit.md +46 -0
- package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
- package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
- package/src/modules/primitives/index.ts +40 -0
- package/src/modules/primitives/lib/errors.ts +138 -0
- package/src/modules/primitives/lib/types.ts +20 -0
- package/src/modules/primitives/module.ts +66 -0
- package/src/modules/primitives/permissions.ts +18 -0
- package/src/modules/primitives/tailor.config.ts +11 -0
- package/src/modules/primitives/testing/fixtures.ts +161 -0
- package/src/modules/product-management/.gitkeep +0 -0
- package/src/modules/purchase/.gitkeep +0 -0
- package/src/modules/sales/.gitkeep +0 -0
- package/src/modules/shared/createContext.test.ts +39 -0
- package/src/modules/shared/createContext.ts +15 -0
- package/src/modules/shared/defineCommand.test.ts +42 -0
- package/src/modules/shared/defineCommand.ts +19 -0
- package/src/modules/shared/definePermissions.test.ts +146 -0
- package/src/modules/shared/definePermissions.ts +94 -0
- package/src/modules/shared/entityTypes.ts +15 -0
- package/src/modules/shared/errors.ts +22 -0
- package/src/modules/shared/index.ts +1 -0
- package/src/modules/shared/internal.ts +13 -0
- package/src/modules/shared/requirePermission.test.ts +47 -0
- package/src/modules/shared/requirePermission.ts +8 -0
- package/src/modules/shared/types.ts +4 -0
- package/src/modules/supplier-management/.gitkeep +0 -0
- package/src/modules/supplier-portal/.gitkeep +0 -0
- package/src/modules/testing/index.ts +120 -0
- package/src/modules/user-management/README.md +38 -0
- package/src/modules/user-management/command/activateUser.test.ts +112 -0
- package/src/modules/user-management/command/activateUser.ts +67 -0
- package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
- package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
- package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
- package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
- package/src/modules/user-management/command/createPermission.test.ts +143 -0
- package/src/modules/user-management/command/createPermission.ts +66 -0
- package/src/modules/user-management/command/createRole.test.ts +115 -0
- package/src/modules/user-management/command/createRole.ts +52 -0
- package/src/modules/user-management/command/createUser.test.ts +198 -0
- package/src/modules/user-management/command/createUser.ts +85 -0
- package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
- package/src/modules/user-management/command/deactivateUser.ts +67 -0
- package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
- package/src/modules/user-management/command/logAuditEvent.ts +59 -0
- package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
- package/src/modules/user-management/command/reactivateUser.ts +67 -0
- package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
- package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
- package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
- package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
- package/src/modules/user-management/db/auditEvent.ts +47 -0
- package/src/modules/user-management/db/permission.ts +31 -0
- package/src/modules/user-management/db/role.ts +28 -0
- package/src/modules/user-management/db/rolePermission.ts +44 -0
- package/src/modules/user-management/db/user.ts +38 -0
- package/src/modules/user-management/db/userRole.ts +44 -0
- package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
- package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
- package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
- package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
- package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
- package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
- package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
- package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
- package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
- package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
- package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
- package/src/modules/user-management/docs/features/audit-trail.md +80 -0
- package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
- package/src/modules/user-management/docs/features/user-account-management.md +64 -0
- package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
- package/src/modules/user-management/docs/models/Permission.md +31 -0
- package/src/modules/user-management/docs/models/Role.md +31 -0
- package/src/modules/user-management/docs/models/RolePermission.md +33 -0
- package/src/modules/user-management/docs/models/User.md +47 -0
- package/src/modules/user-management/docs/models/UserRole.md +34 -0
- package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
- package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
- package/src/modules/user-management/generated/enums.ts +24 -0
- package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
- package/src/modules/user-management/index.ts +32 -0
- package/src/modules/user-management/lib/errors.ts +81 -0
- package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
- package/src/modules/user-management/lib/types.ts +31 -0
- package/src/modules/user-management/module.ts +77 -0
- package/src/modules/user-management/permissions.ts +15 -0
- package/src/modules/user-management/tailor.config.ts +11 -0
- package/src/modules/user-management/testing/fixtures.ts +98 -0
- package/src/schemas.ts +25 -0
- package/src/testing.ts +10 -0
- package/src/util.ts +3 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "examples/**/src/pages/**/page.tsx"
|
|
4
|
+
- "examples/**/src/pages/**/components/*table*.tsx"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ListView Screen Implementation
|
|
8
|
+
|
|
9
|
+
Implementation pattern for screens with `Screen Type: ListView`.
|
|
10
|
+
Assumes `page.md` and `component.md` rules.
|
|
11
|
+
|
|
12
|
+
## File Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
{screen-path}/
|
|
16
|
+
├── components/
|
|
17
|
+
│ └── {screen-name}-table.tsx # Table component with fragments
|
|
18
|
+
└── page.tsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Page Component (page.tsx)
|
|
22
|
+
|
|
23
|
+
Data fetching and `Layout` must be co-located in the same page component.
|
|
24
|
+
Do NOT split into an inner Content component — `Layout` requires `Layout.Column` as direct children.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
const ResourcesQuery = graphql(
|
|
28
|
+
`
|
|
29
|
+
query Resources {
|
|
30
|
+
resources {
|
|
31
|
+
...ResourceTable
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
[ResourceTableFragment],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const ResourcesPage = () => {
|
|
39
|
+
const [{ data, error, fetching }, reexecuteQuery] = useQuery({
|
|
40
|
+
query: ResourcesQuery,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (fetching) return <Loading />;
|
|
44
|
+
|
|
45
|
+
if (error || !data) {
|
|
46
|
+
return (
|
|
47
|
+
<ErrorFallback
|
|
48
|
+
title="Failed to load resources"
|
|
49
|
+
message="An error occurred while fetching the list."
|
|
50
|
+
onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Layout
|
|
57
|
+
columns={1}
|
|
58
|
+
title="Resources"
|
|
59
|
+
actions={[
|
|
60
|
+
<Button key="create" asChild>
|
|
61
|
+
<Link to="create">Create</Link>
|
|
62
|
+
</Button>,
|
|
63
|
+
]}
|
|
64
|
+
>
|
|
65
|
+
<Layout.Column>
|
|
66
|
+
<ResourceTable data={data.resources} />
|
|
67
|
+
</Layout.Column>
|
|
68
|
+
</Layout>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Table Component (components/{screen-name}-table.tsx)
|
|
74
|
+
|
|
75
|
+
### Fragment Collocation
|
|
76
|
+
|
|
77
|
+
Define a row fragment and a table fragment wrapping the Connection type.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
const ResourceRowFragment = graphql(`
|
|
81
|
+
fragment ResourceRow on Resource {
|
|
82
|
+
id
|
|
83
|
+
title
|
|
84
|
+
status
|
|
85
|
+
createdAt
|
|
86
|
+
}
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
export const ResourceTableFragment = graphql(
|
|
90
|
+
`
|
|
91
|
+
fragment ResourceTable on ResourceConnection {
|
|
92
|
+
edges {
|
|
93
|
+
node {
|
|
94
|
+
...ResourceRow
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`,
|
|
99
|
+
[ResourceRowFragment],
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Row Component
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const ResourceRow = ({ resource: resourceFragment }: ResourceRowProps) => {
|
|
107
|
+
const resource = readFragment(ResourceRowFragment, resourceFragment);
|
|
108
|
+
return (
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableCell>{resource.title}</TableCell>
|
|
111
|
+
<TableCell>
|
|
112
|
+
<Badge variant={resource.status === "ACTIVE" ? "default" : "secondary"}>
|
|
113
|
+
{resource.status}
|
|
114
|
+
</Badge>
|
|
115
|
+
</TableCell>
|
|
116
|
+
<TableCell>
|
|
117
|
+
<Button variant="ghost" size="sm" asChild>
|
|
118
|
+
<Link to={resource.id}>View</Link>
|
|
119
|
+
</Button>
|
|
120
|
+
</TableCell>
|
|
121
|
+
</TableRow>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Empty State
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
if (connection.edges.length === 0) {
|
|
130
|
+
return (
|
|
131
|
+
<EmptyState
|
|
132
|
+
title="No resources"
|
|
133
|
+
message="Get started by creating a new resource."
|
|
134
|
+
action={
|
|
135
|
+
<Button asChild>
|
|
136
|
+
<Link to="create">Create</Link>
|
|
137
|
+
</Button>
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Column Property Mapping
|
|
145
|
+
|
|
146
|
+
| Property | Implementation |
|
|
147
|
+
| --------------- | ----------------------------------------------------------- |
|
|
148
|
+
| Hideable: Yes | Column visibility state to toggle show/hide |
|
|
149
|
+
| Sortable: Yes | Sort icon on `<TableHead>` + onClick to toggle query vars |
|
|
150
|
+
| Filterable: Yes | Filter UI above table (`<Select />`) |
|
|
151
|
+
| Searchable: Yes | Search input above table (`<Input placeholder="Search" />`) |
|
|
152
|
+
|
|
153
|
+
## Key Points
|
|
154
|
+
|
|
155
|
+
- Use `<Badge />` for status columns
|
|
156
|
+
- Format dates with `toLocaleDateString()`
|
|
157
|
+
- Use `<EmptyState />` with Create action for empty lists
|
|
158
|
+
- Add a View button per row to navigate to the detail page
|
|
159
|
+
- Iterate data using Connection type `edges.node` pattern
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "examples/**/src/"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Application Directory Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
{app_name}/
|
|
10
|
+
├── backend/
|
|
11
|
+
│ ├── src/
|
|
12
|
+
│ │ ├── modules.ts # Declaring module usage
|
|
13
|
+
│ │ ├── modules/
|
|
14
|
+
│ │ │ └── {module-name}/ # Module-specific directory
|
|
15
|
+
│ │ │ ├── resolvers/ # API Definition to expose graphql apis
|
|
16
|
+
│ │ │ └── executors/ # PubSub Automation (one file per declaration)
|
|
17
|
+
│ │ └── generated/ # Auto-generated code (do not edit)
|
|
18
|
+
│ └── tailor.config.ts # tailor application config
|
|
19
|
+
│
|
|
20
|
+
└── frontend/
|
|
21
|
+
└── src/
|
|
22
|
+
├── pages/ # File-based routing (auto-discovered by Vite plugin)
|
|
23
|
+
│ └── {page-path}/
|
|
24
|
+
│ ├── page.tsx
|
|
25
|
+
│ └── {page-path}/
|
|
26
|
+
│ ├── components/
|
|
27
|
+
│ └── page.tsx
|
|
28
|
+
├── components/
|
|
29
|
+
│ └── ui/ # Generic UI components
|
|
30
|
+
├── graphql/ # gql.tada settings
|
|
31
|
+
└── providers/ # react providers
|
|
32
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/command/*.ts"
|
|
4
|
+
- "!modules/*/src/command/*.test.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Command Implementation
|
|
8
|
+
|
|
9
|
+
## defineCommand Pattern
|
|
10
|
+
|
|
11
|
+
Commands that don't need custom fields use `defineCommand` directly:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
export const myCommand = defineCommand(permissions.myCommand, async (db: DB, input: Input) => { ... });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Factory Function Pattern (custom fields)
|
|
18
|
+
|
|
19
|
+
Commands that insert into a table with user-extensible fields use a `makeCreateX<CF>()` factory:
|
|
20
|
+
|
|
21
|
+
- Export only `makeCreateX` — no default instance
|
|
22
|
+
- Generic `CF extends Record<string, unknown>` for custom fields
|
|
23
|
+
- Input type: `CreateXInput & CF`
|
|
24
|
+
- Destructure known fields, rest-spread custom fields into `.values()` before known fields
|
|
25
|
+
- Cast custom fields: `...(customFields as Record<string, unknown>)`
|
|
26
|
+
- `module.ts` wires with `makeCreateX<FieldsToInsertable<F>>()`
|
|
27
|
+
|
|
28
|
+
## Implementation Considerations
|
|
29
|
+
|
|
30
|
+
- **Validation**: Check referenced entities exist before operating
|
|
31
|
+
- **Idempotency**: For assign/revoke, return existing instead of throwing
|
|
32
|
+
- **Return format**: Wrap in object `{ entity }` not just `entity`
|
|
33
|
+
|
|
34
|
+
## Conventions
|
|
35
|
+
|
|
36
|
+
- Input types: exported interfaces (`export interface MyFunctionInput`)
|
|
37
|
+
- Use `.executeTakeFirst()` for single results
|
|
38
|
+
- Include JSDoc: `/** Function: name \n Description */`
|
|
39
|
+
|
|
40
|
+
## State Transitions
|
|
41
|
+
|
|
42
|
+
For commands that transition between statuses, accept `from?: string[]` with a default:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
from?: string[]; // Default: ["ACTIVE"]
|
|
46
|
+
|
|
47
|
+
const validFromStatuses = input.from ?? ["ACTIVE"];
|
|
48
|
+
if (!validFromStatuses.includes(user.status)) {
|
|
49
|
+
throw new InvalidStatusTransitionError(user.status, targetStatus);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Default `from` contains the base valid source status
|
|
54
|
+
- Parent modules can override to allow transitions from additional statuses
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
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)
|
|
@@ -0,0 +1,67 @@
|
|
|
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.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/index.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Module Exports
|
|
7
|
+
|
|
8
|
+
Export order:
|
|
9
|
+
|
|
10
|
+
1. `defineModule` from module.ts
|
|
11
|
+
2. Generated types (enum values + types separately)
|
|
12
|
+
3. Error classes (for typed catch blocks)
|
|
13
|
+
4. Domain functions with input types
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/db/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Models
|
|
7
|
+
|
|
8
|
+
## Factory Function Pattern
|
|
9
|
+
|
|
10
|
+
Each model is a `createXType<const F>(params)` factory:
|
|
11
|
+
|
|
12
|
+
- Params interface generic: `F extends Record<string, TailorAnyDBField>`
|
|
13
|
+
- `fields?: F` — optional custom fields from parent modules
|
|
14
|
+
- Spread as `...(params.fields ?? {}) as F` to preserve type information
|
|
15
|
+
- Include `...db.fields.timestamps()`
|
|
16
|
+
- Use `.description()` for field docs
|
|
17
|
+
- Apply permissions at model level
|
|
18
|
+
- Export a default instance: `export const x = createXType({})`
|
|
19
|
+
|
|
20
|
+
## Stateful Model Enums
|
|
21
|
+
|
|
22
|
+
Status enums are tied to the module's state machine. Base module defines core statuses; parent callers can only **extend, not replace**.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// Good: extension only
|
|
26
|
+
const BASE_STATUSES = ["PENDING", "ACTIVE", "INACTIVE"] as const;
|
|
27
|
+
const statuses = [...BASE_STATUSES, ...(params.additionalStatuses ?? [])];
|
|
28
|
+
|
|
29
|
+
// Bad: allows replacement
|
|
30
|
+
const statuses = params.statuses ?? ["PENDING", "ACTIVE", "INACTIVE"];
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Name parameter `additionalX` to signal extension-only
|
|
34
|
+
- Parent modules handle their additional status transitions
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Module Directory Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/
|
|
10
|
+
├── db/ # Database models (one file per model)
|
|
11
|
+
├── executor/ # Async executors (record triggers, job functions)
|
|
12
|
+
├── function/ # Domain functions + tests (*.test.ts co-located)
|
|
13
|
+
├── lib/ # Internal shared code (errors.ts, types.ts)
|
|
14
|
+
├── testing/ # Test fixtures and helpers
|
|
15
|
+
├── generated/ # Auto-generated code (do not edit)
|
|
16
|
+
├── index.ts # Public exports
|
|
17
|
+
└── module.ts # Module definition
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
- `db/`: Only documentable model definitions, no helpers
|
|
23
|
+
- `executor/`: Async executors as factory functions (see executors.md)
|
|
24
|
+
- `function/`: Domain functions + co-located tests, no utilities
|
|
25
|
+
- `lib/`: Internal errors and types (not documented)
|
|
26
|
+
- `testing/`: Fixtures for tests only
|
|
27
|
+
- Run `pnpm generate` after modifying `db/` models
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sync vs Async Operations
|
|
7
|
+
|
|
8
|
+
## Decision Criteria
|
|
9
|
+
|
|
10
|
+
Choose synchronous or asynchronous execution based on:
|
|
11
|
+
|
|
12
|
+
1. **Number of affected records** - How many records need processing?
|
|
13
|
+
2. **Data growth trajectory** - Will the operation slow down as data grows?
|
|
14
|
+
|
|
15
|
+
| Scenario | Approach | Implementation |
|
|
16
|
+
| ----------------------------------- | ------------------ | --------------------------- |
|
|
17
|
+
| Single record, bounded complexity | **Synchronous** | Inline in command |
|
|
18
|
+
| Single record, unbounded complexity | **Consider async** | Evaluate growth |
|
|
19
|
+
| Multiple records | **Asynchronous** | Executor with `jobFunction` |
|
|
20
|
+
|
|
21
|
+
## Growth Considerations
|
|
22
|
+
|
|
23
|
+
Ask: "How does this operation scale as the system grows?"
|
|
24
|
+
|
|
25
|
+
- **Bounded**: User status update (always 1 record, O(1))
|
|
26
|
+
- **Bounded**: Single user permission recompute (limited by reasonable role/permission counts)
|
|
27
|
+
- **Unbounded**: All users with role X (grows with user base, O(n))
|
|
28
|
+
- **Unbounded**: All orders in date range (grows with transaction volume)
|
|
29
|
+
|
|
30
|
+
When in doubt, monitor operation timing in production and migrate to async if latency increases.
|
|
31
|
+
|
|
32
|
+
## Rationale
|
|
33
|
+
|
|
34
|
+
- **Synchronous**: Acceptable when operation time is predictable and bounded
|
|
35
|
+
- **Asynchronous**: Required when operation time scales with data volume
|
|
36
|
+
|
|
37
|
+
## Pattern: Synchronous (Single Record)
|
|
38
|
+
|
|
39
|
+
Commands that affect a single known record should execute the operation inline and return the result:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// In command - recompute immediately
|
|
43
|
+
const updatedUser = await recomputeUserPermissions(db, input.userId);
|
|
44
|
+
return { userRole, user: updatedUser, auditEvent };
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Pattern: Asynchronous (Multiple Records)
|
|
48
|
+
|
|
49
|
+
Commands that affect an unknown number of records should delegate to an executor:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Command just creates/deletes the record
|
|
53
|
+
// Executor handles recomputation asynchronously
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Create executors with record triggers:
|
|
57
|
+
|
|
58
|
+
- `recordCreatedTrigger` - React to inserts
|
|
59
|
+
- `recordDeletedTrigger` - React to deletes
|
|
60
|
+
- Use `kind: "jobFunction"` for extended execution
|
|
61
|
+
|
|
62
|
+
## Example: Permission Recomputation
|
|
63
|
+
|
|
64
|
+
| Command | Affected | Approach |
|
|
65
|
+
| ------------------------ | ----------------------- | --------------- |
|
|
66
|
+
| assignRoleToUser | 1 user | Sync in command |
|
|
67
|
+
| revokeRoleFromUser | 1 user | Sync in command |
|
|
68
|
+
| assignPermissionToRole | N users (all with role) | Async executor |
|
|
69
|
+
| revokePermissionFromRole | N users (all with role) | Async executor |
|
|
70
|
+
|
|
71
|
+
## Trade-offs
|
|
72
|
+
|
|
73
|
+
**Synchronous:**
|
|
74
|
+
|
|
75
|
+
- Immediate consistency
|
|
76
|
+
- Simpler error handling
|
|
77
|
+
- Blocks request until complete
|
|
78
|
+
|
|
79
|
+
**Asynchronous:**
|
|
80
|
+
|
|
81
|
+
- Eventual consistency
|
|
82
|
+
- Non-blocking requests
|
|
83
|
+
- Requires executor infrastructure
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/command/*.test.ts"
|
|
4
|
+
- "modules/*/src/testing/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Testing Patterns
|
|
8
|
+
|
|
9
|
+
## Test Coverage Goal
|
|
10
|
+
|
|
11
|
+
Tests should cover all paths in the corresponding `docs/commands/*.md`:
|
|
12
|
+
|
|
13
|
+
- **Process Flow**: Each branch in the mermaid flowchart = one test case
|
|
14
|
+
- **Error Scenarios**: Each error code listed = one test case
|
|
15
|
+
- **Idempotent paths**: If flowchart shows "Already exists? → Return existing"
|
|
16
|
+
|
|
17
|
+
## Mock Database
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
const { db, spies } = createMockDb<DB>();
|
|
21
|
+
|
|
22
|
+
// Single return
|
|
23
|
+
spies.select.mockReturnValue(entity);
|
|
24
|
+
|
|
25
|
+
// Sequential returns (in query execution order)
|
|
26
|
+
spies.select.mockReturnValueOnce(first).mockReturnValueOnce(second);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Custom Fields Passthrough
|
|
30
|
+
|
|
31
|
+
For `makeCreateX` factory commands, add one test verifying custom fields reach `.values()`:
|
|
32
|
+
|
|
33
|
+
- Instantiate with concrete type: `makeCreateX<{ myField: string }>()`
|
|
34
|
+
- Assert with `spies.values`: `expect(spies.values).toHaveBeenNthCalledWith(1, expect.objectContaining({ myField: "value" }))`
|
|
35
|
+
- Use `toHaveBeenNthCalledWith(n, ...)` when multiple inserts occur (e.g., audit events)
|
|
36
|
+
|
|
37
|
+
## Fixtures (`src/testing/fixtures.ts`)
|
|
38
|
+
|
|
39
|
+
- Import `Schema` from `lib/types` (not `Namespace` from generated code)
|
|
40
|
+
- Pattern: `export const baseEntity = { ... } as const satisfies Entity<Schema>`
|
|
41
|
+
- Fixed IDs for traceability: `"entity-1"`
|
|
42
|
+
- Consistent timestamp: `new Date("2024-01-01T00:00:00.000Z")`
|
|
43
|
+
- `updatedAt: null` for base fixtures
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/db/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Relations
|
|
7
|
+
|
|
8
|
+
## Relation Pattern
|
|
9
|
+
|
|
10
|
+
Use `.relation()` for foreign keys instead of plain `db.uuid()`:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
userId: db.uuid().relation({
|
|
14
|
+
type: "n-1",
|
|
15
|
+
toward: { type: user },
|
|
16
|
+
backward: "userRoles",
|
|
17
|
+
}).description("Foreign key to User"),
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Parameters
|
|
21
|
+
|
|
22
|
+
- `type`: `"n-1"` (many-to-one), `"1-1"` (one-to-one), or `"keyOnly"` (FK only, no navigation)
|
|
23
|
+
- `toward.type`: The related type definition (import it)
|
|
24
|
+
- `backward`: Field name for reverse navigation from the related type
|
|
25
|
+
|
|
26
|
+
### Benefits over plain `db.uuid()`
|
|
27
|
+
|
|
28
|
+
- Foreign key constraints (referential integrity)
|
|
29
|
+
- Forward navigation: `UserRole.user`
|
|
30
|
+
- Backward navigation: `User.userRoles`
|
|
31
|
+
- GraphQL relational queries
|
|
32
|
+
|
|
33
|
+
## Junction Tables (Many-to-Many)
|
|
34
|
+
|
|
35
|
+
Junction tables need relations on both foreign keys plus a composite unique index:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { user } from "./user";
|
|
39
|
+
import { role } from "./role";
|
|
40
|
+
|
|
41
|
+
db.type("UserRole", {
|
|
42
|
+
userId: db.uuid().relation({
|
|
43
|
+
type: "n-1",
|
|
44
|
+
toward: { type: user },
|
|
45
|
+
backward: "userRoles",
|
|
46
|
+
}),
|
|
47
|
+
roleId: db.uuid().relation({
|
|
48
|
+
type: "n-1",
|
|
49
|
+
toward: { type: role },
|
|
50
|
+
backward: "userRoles",
|
|
51
|
+
}),
|
|
52
|
+
}).indexes({
|
|
53
|
+
fields: ["userId", "roleId"],
|
|
54
|
+
unique: true,
|
|
55
|
+
name: "user_role_unique_idx",
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Naming Conventions
|
|
60
|
+
|
|
61
|
+
- `backward` field: Use plural of the junction table name (e.g., `"userRoles"`, `"rolePermissions"`)
|
|
62
|
+
- Index name: `{table}_unique_idx` pattern
|
|
63
|
+
|
|
64
|
+
## Composite Indexes
|
|
65
|
+
|
|
66
|
+
Use `.indexes()` (not `.uniqueIndex()`) for multi-column constraints:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
.indexes({
|
|
70
|
+
fields: ["fieldA", "fieldB"],
|
|
71
|
+
unique: true,
|
|
72
|
+
name: "descriptive_idx_name",
|
|
73
|
+
})
|
|
74
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "modules/*/src/db/*.ts"
|
|
4
|
+
- "modules/*/src/executor/*.ts"
|
|
5
|
+
- "modules/*/src/workflow/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SDK Docs Reference
|
|
9
|
+
|
|
10
|
+
Read the official Tailor SDK documentation for database models, executors:
|
|
11
|
+
|
|
12
|
+
- [TailorDB Models](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/tailordb.md)
|
|
13
|
+
- [Executors](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/executors.md)
|
|
14
|
+
- [Workflow](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/workflow.md)
|