@tailor-platform/erp-kit 0.1.0 → 0.1.1
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 +7 -0
- package/LICENSE +21 -0
- package/dist/cli.js +64 -44
- package/package.json +4 -2
- 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 +2 -2
- package/src/commands/init.test.ts +30 -19
- package/src/commands/init.ts +76 -43
- 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/commands/init.ts
CHANGED
|
@@ -4,7 +4,6 @@ import chalk from "chalk";
|
|
|
4
4
|
import { PACKAGE_ROOT } from "../util.js";
|
|
5
5
|
|
|
6
6
|
const SKILLS_SRC = path.join(PACKAGE_ROOT, "skills");
|
|
7
|
-
const RULES_SRC = path.join(PACKAGE_ROOT, "rules");
|
|
8
7
|
|
|
9
8
|
const FRAMEWORK_SKILLS = [
|
|
10
9
|
"1-module-docs",
|
|
@@ -21,65 +20,99 @@ const FRAMEWORK_SKILLS = [
|
|
|
21
20
|
"mock-scenario",
|
|
22
21
|
];
|
|
23
22
|
|
|
23
|
+
function copyDirectoryRecursive(
|
|
24
|
+
srcDir: string,
|
|
25
|
+
destDir: string,
|
|
26
|
+
force: boolean,
|
|
27
|
+
): { copied: number; skipped: number } {
|
|
28
|
+
let copied = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
|
|
31
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
32
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
33
|
+
const destPath = path.join(destDir, entry.name);
|
|
34
|
+
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
const sub = copyDirectoryRecursive(srcPath, destPath, force);
|
|
37
|
+
copied += sub.copied;
|
|
38
|
+
skipped += sub.skipped;
|
|
39
|
+
} else {
|
|
40
|
+
if (!force && fs.existsSync(destPath)) {
|
|
41
|
+
skipped++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
45
|
+
fs.copyFileSync(srcPath, destPath);
|
|
46
|
+
copied++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { copied, skipped };
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
export function runInit(cwd: string, force: boolean): number {
|
|
25
54
|
console.log(chalk.bold("erp-kit init\n"));
|
|
26
55
|
|
|
56
|
+
// --- Skills ---
|
|
27
57
|
const skillsDest = path.join(cwd, ".agents", "skills");
|
|
28
58
|
let copiedCount = 0;
|
|
29
59
|
let skippedCount = 0;
|
|
30
60
|
for (const skill of FRAMEWORK_SKILLS) {
|
|
31
|
-
const
|
|
61
|
+
const srcSkillDir = path.join(SKILLS_SRC, skill);
|
|
62
|
+
if (!fs.existsSync(srcSkillDir)) continue;
|
|
63
|
+
|
|
32
64
|
const destDir = path.join(skillsDest, skill);
|
|
33
|
-
const
|
|
65
|
+
const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
|
|
66
|
+
copiedCount += result.copied;
|
|
34
67
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
skippedCount++;
|
|
39
|
-
continue;
|
|
68
|
+
if (result.skipped > 0) {
|
|
69
|
+
console.log(chalk.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
|
|
70
|
+
skippedCount += result.skipped;
|
|
40
71
|
}
|
|
41
|
-
|
|
42
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
43
|
-
fs.copyFileSync(srcSkill, destSkill);
|
|
44
|
-
copiedCount++;
|
|
45
72
|
}
|
|
46
|
-
console.log(chalk.green(` Copied ${copiedCount}
|
|
73
|
+
console.log(chalk.green(` Copied ${copiedCount} skill files to .agents/skills/`));
|
|
47
74
|
if (skippedCount > 0) {
|
|
48
75
|
console.log(
|
|
49
|
-
chalk.yellow(` Skipped ${skippedCount} existing
|
|
76
|
+
chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`),
|
|
50
77
|
);
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
// --- Claude Code symlink ---
|
|
81
|
+
const claudeSkills = path.join(cwd, ".claude", "skills");
|
|
82
|
+
const relTarget = path.relative(path.dirname(claudeSkills), skillsDest);
|
|
83
|
+
|
|
84
|
+
// lstatSync doesn't follow symlinks, so dangling symlinks are detected
|
|
85
|
+
const claudeSkillsExists = (() => {
|
|
86
|
+
try {
|
|
87
|
+
fs.lstatSync(claudeSkills);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
if (claudeSkillsExists) {
|
|
95
|
+
const stat = fs.lstatSync(claudeSkills);
|
|
96
|
+
if (stat.isSymbolicLink()) {
|
|
97
|
+
const current = fs.readlinkSync(claudeSkills);
|
|
98
|
+
if (current === relTarget) {
|
|
99
|
+
console.log(chalk.green(" .claude/skills -> .agents/skills/ (already linked)"));
|
|
100
|
+
} else if (force) {
|
|
101
|
+
fs.unlinkSync(claudeSkills);
|
|
102
|
+
fs.symlinkSync(relTarget, claudeSkills);
|
|
103
|
+
console.log(chalk.green(" .claude/skills -> .agents/skills/ (relinked)"));
|
|
104
|
+
} else {
|
|
105
|
+
console.log(
|
|
106
|
+
chalk.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`),
|
|
107
|
+
);
|
|
74
108
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
fs.mkdirSync(path.dirname(claudeSkills), { recursive: true });
|
|
114
|
+
fs.symlinkSync(relTarget, claudeSkills);
|
|
115
|
+
console.log(chalk.green(" .claude/skills -> .agents/skills/ (linked)"));
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
console.log(chalk.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
|
|
@@ -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.
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
|
@@ -1,14 +0,0 @@
|
|
|
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)
|