@tailor-platform/create-sdk 0.0.1 → 0.8.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 +448 -0
- package/LICENSE +21 -0
- package/README.md +23 -33
- package/dist/index.js +180 -0
- package/package.json +41 -8
- package/templates/hello-world/.gitignore +3 -0
- package/templates/hello-world/.prettierignore +1 -0
- package/templates/hello-world/.prettierrc +1 -0
- package/templates/hello-world/README.md +59 -0
- package/templates/hello-world/eslint.config.js +27 -0
- package/templates/hello-world/package.json +22 -0
- package/templates/hello-world/src/resolvers/hello.ts +19 -0
- package/templates/hello-world/tailor.config.ts +6 -0
- package/templates/hello-world/tsconfig.json +15 -0
- package/templates/inventory-management/.gitignore +3 -0
- package/templates/inventory-management/.prettierignore +2 -0
- package/templates/inventory-management/.prettierrc +1 -0
- package/templates/inventory-management/README.md +246 -0
- package/templates/inventory-management/eslint.config.js +33 -0
- package/templates/inventory-management/package.json +28 -0
- package/templates/inventory-management/src/db/category.ts +13 -0
- package/templates/inventory-management/src/db/common/permission.ts +59 -0
- package/templates/inventory-management/src/db/contact.ts +17 -0
- package/templates/inventory-management/src/db/inventory.ts +18 -0
- package/templates/inventory-management/src/db/notification.ts +21 -0
- package/templates/inventory-management/src/db/order.ts +20 -0
- package/templates/inventory-management/src/db/orderItem.ts +36 -0
- package/templates/inventory-management/src/db/product.ts +18 -0
- package/templates/inventory-management/src/db/user.ts +12 -0
- package/templates/inventory-management/src/executor/checkInventory.ts +28 -0
- package/templates/inventory-management/src/generated/kysely-tailordb.ts +92 -0
- package/templates/inventory-management/src/pipeline/registerOrder.ts +100 -0
- package/templates/inventory-management/tailor.config.ts +35 -0
- package/templates/inventory-management/tsconfig.json +16 -0
- package/templates/testing/.gitignore +3 -0
- package/templates/testing/.prettierignore +2 -0
- package/templates/testing/.prettierrc +1 -0
- package/templates/testing/README.md +130 -0
- package/templates/testing/e2e/globalSetup.ts +65 -0
- package/templates/testing/e2e/resolver.test.ts +57 -0
- package/templates/testing/eslint.config.js +27 -0
- package/templates/testing/package.json +33 -0
- package/templates/testing/src/db/user.ts +22 -0
- package/templates/testing/src/generated/db.ts +28 -0
- package/templates/testing/src/resolver/mockTailordb.test.ts +71 -0
- package/templates/testing/src/resolver/mockTailordb.ts +44 -0
- package/templates/testing/src/resolver/simple.test.ts +13 -0
- package/templates/testing/src/resolver/simple.ts +16 -0
- package/templates/testing/src/resolver/wrapTailordb.test.ts +53 -0
- package/templates/testing/src/resolver/wrapTailordb.ts +80 -0
- package/templates/testing/tailor.config.ts +23 -0
- package/templates/testing/tsconfig.json +16 -0
- package/templates/testing/vitest.config.ts +22 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inventory-management",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"deploy": "tailor-sdk apply",
|
|
7
|
+
"generate": "tailor-sdk generate",
|
|
8
|
+
"format": "prettier --write .",
|
|
9
|
+
"format:check": "prettier --check .",
|
|
10
|
+
"lint": "eslint --cache .",
|
|
11
|
+
"lint:fix": "eslint --cache --fix .",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@tailor-platform/function-kysely-tailordb": "0.1.3",
|
|
16
|
+
"kysely": "0.28.7"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "9.39.1",
|
|
20
|
+
"@tailor-platform/function-types": "0.7.2",
|
|
21
|
+
"@tailor-platform/sdk": "0.8.1",
|
|
22
|
+
"@types/node": "22.19.0",
|
|
23
|
+
"eslint": "9.39.1",
|
|
24
|
+
"prettier": "3.6.2",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"typescript-eslint": "8.46.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { gqlPermissionManager, permissionManager } from "./common/permission";
|
|
3
|
+
|
|
4
|
+
export const category = db
|
|
5
|
+
.type("Category", {
|
|
6
|
+
name: db.string().description("Name of the category").unique(),
|
|
7
|
+
description: db
|
|
8
|
+
.string({ optional: true })
|
|
9
|
+
.description("Description of the category"),
|
|
10
|
+
...db.fields.timestamps(),
|
|
11
|
+
})
|
|
12
|
+
.permission(permissionManager)
|
|
13
|
+
.gqlPermission(gqlPermissionManager);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PermissionCondition,
|
|
3
|
+
TailorTypeGqlPermission,
|
|
4
|
+
TailorTypePermission,
|
|
5
|
+
} from "@tailor-platform/sdk";
|
|
6
|
+
|
|
7
|
+
export interface User {
|
|
8
|
+
role: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const managerRole = [
|
|
12
|
+
{ user: "role" },
|
|
13
|
+
"=",
|
|
14
|
+
"MANAGER",
|
|
15
|
+
] as const satisfies PermissionCondition;
|
|
16
|
+
export const loggedIn = [
|
|
17
|
+
{ user: "_loggedIn" },
|
|
18
|
+
"=",
|
|
19
|
+
true,
|
|
20
|
+
] as const satisfies PermissionCondition;
|
|
21
|
+
|
|
22
|
+
// Manager can do anything, Staff can only read.
|
|
23
|
+
export const permissionManager = {
|
|
24
|
+
create: [managerRole],
|
|
25
|
+
read: [loggedIn],
|
|
26
|
+
update: [managerRole],
|
|
27
|
+
delete: [managerRole],
|
|
28
|
+
} as const satisfies TailorTypePermission;
|
|
29
|
+
|
|
30
|
+
// Manager can perform any GraphQL operations, Staff can only read.
|
|
31
|
+
export const gqlPermissionManager = [
|
|
32
|
+
{
|
|
33
|
+
conditions: [managerRole],
|
|
34
|
+
actions: "all",
|
|
35
|
+
permit: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
conditions: [loggedIn],
|
|
39
|
+
actions: ["read"],
|
|
40
|
+
permit: true,
|
|
41
|
+
},
|
|
42
|
+
] as const satisfies TailorTypeGqlPermission;
|
|
43
|
+
|
|
44
|
+
// Any logged-in user can do anything.
|
|
45
|
+
export const permissionLoggedIn = {
|
|
46
|
+
create: [loggedIn],
|
|
47
|
+
read: [loggedIn],
|
|
48
|
+
update: [loggedIn],
|
|
49
|
+
delete: [loggedIn],
|
|
50
|
+
} as const satisfies TailorTypePermission;
|
|
51
|
+
|
|
52
|
+
// Any logged-in user can perform read GraphQL operation.
|
|
53
|
+
export const gqlPermissionLoggedIn = [
|
|
54
|
+
{
|
|
55
|
+
conditions: [loggedIn],
|
|
56
|
+
actions: ["read"],
|
|
57
|
+
permit: true,
|
|
58
|
+
},
|
|
59
|
+
] as const satisfies TailorTypeGqlPermission;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { gqlPermissionManager, permissionManager } from "./common/permission";
|
|
3
|
+
|
|
4
|
+
export const contact = db
|
|
5
|
+
.type("Contact", {
|
|
6
|
+
name: db.string().description("Name of the contact"),
|
|
7
|
+
email: db.string().unique().description("Email address of the contact"),
|
|
8
|
+
phone: db
|
|
9
|
+
.string({ optional: true })
|
|
10
|
+
.description("Phone number of the contact"),
|
|
11
|
+
address: db
|
|
12
|
+
.string({ optional: true })
|
|
13
|
+
.description("Address of the contact"),
|
|
14
|
+
...db.fields.timestamps(),
|
|
15
|
+
})
|
|
16
|
+
.permission(permissionManager)
|
|
17
|
+
.gqlPermission(gqlPermissionManager);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission";
|
|
3
|
+
import { product } from "./product";
|
|
4
|
+
|
|
5
|
+
export const inventory = db
|
|
6
|
+
.type("Inventory", {
|
|
7
|
+
productId: db
|
|
8
|
+
.uuid()
|
|
9
|
+
.description("ID of the product")
|
|
10
|
+
.relation({ type: "1-1", toward: { type: product } }),
|
|
11
|
+
quantity: db
|
|
12
|
+
.int()
|
|
13
|
+
.description("Quantity of the product in inventory")
|
|
14
|
+
.validate(({ value }) => value >= 0),
|
|
15
|
+
...db.fields.timestamps(),
|
|
16
|
+
})
|
|
17
|
+
.permission(permissionLoggedIn)
|
|
18
|
+
.gqlPermission(gqlPermissionLoggedIn);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { loggedIn, managerRole, permissionManager } from "./common/permission";
|
|
3
|
+
|
|
4
|
+
export const notification = db
|
|
5
|
+
.type("Notification", {
|
|
6
|
+
message: db.string().description("Notification message"),
|
|
7
|
+
...db.fields.timestamps(),
|
|
8
|
+
})
|
|
9
|
+
.permission(permissionManager)
|
|
10
|
+
.gqlPermission([
|
|
11
|
+
{
|
|
12
|
+
conditions: [managerRole],
|
|
13
|
+
actions: ["delete"],
|
|
14
|
+
permit: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
conditions: [loggedIn],
|
|
18
|
+
actions: ["read"],
|
|
19
|
+
permit: true,
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { contact } from "./contact";
|
|
3
|
+
import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission";
|
|
4
|
+
|
|
5
|
+
export const order = db
|
|
6
|
+
.type("Order", {
|
|
7
|
+
name: db.string().description("Name of the order"),
|
|
8
|
+
description: db
|
|
9
|
+
.string({ optional: true })
|
|
10
|
+
.description("Description of the order"),
|
|
11
|
+
orderDate: db.datetime().description("Date of the order"),
|
|
12
|
+
orderType: db.enum("PURCHASE", "SALES").description("Type of the order"),
|
|
13
|
+
contactId: db
|
|
14
|
+
.uuid()
|
|
15
|
+
.description("Contact associated with the order")
|
|
16
|
+
.relation({ type: "n-1", toward: { type: contact } }),
|
|
17
|
+
...db.fields.timestamps(),
|
|
18
|
+
})
|
|
19
|
+
.permission(permissionLoggedIn)
|
|
20
|
+
.gqlPermission(gqlPermissionLoggedIn);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { order } from "./order";
|
|
3
|
+
import { product } from "./product";
|
|
4
|
+
import { gqlPermissionLoggedIn, permissionLoggedIn } from "./common/permission";
|
|
5
|
+
|
|
6
|
+
export const orderItem = db
|
|
7
|
+
.type("OrderItem", {
|
|
8
|
+
orderId: db
|
|
9
|
+
.uuid()
|
|
10
|
+
.description("ID of the order")
|
|
11
|
+
.relation({ type: "n-1", toward: { type: order } }),
|
|
12
|
+
productId: db
|
|
13
|
+
.uuid()
|
|
14
|
+
.description("ID of the product")
|
|
15
|
+
.relation({ type: "n-1", toward: { type: product } }),
|
|
16
|
+
quantity: db
|
|
17
|
+
.int()
|
|
18
|
+
.description("Quantity of the product")
|
|
19
|
+
.validate(({ value }) => value >= 0),
|
|
20
|
+
unitPrice: db
|
|
21
|
+
.float()
|
|
22
|
+
.description("Unit price of the product")
|
|
23
|
+
.validate(({ value }) => value >= 0),
|
|
24
|
+
totalPrice: db
|
|
25
|
+
.float({ optional: true, assertNonNull: true })
|
|
26
|
+
.description("Total price of the order item"),
|
|
27
|
+
...db.fields.timestamps(),
|
|
28
|
+
})
|
|
29
|
+
.hooks({
|
|
30
|
+
totalPrice: {
|
|
31
|
+
create: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0),
|
|
32
|
+
update: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0),
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.permission(permissionLoggedIn)
|
|
36
|
+
.gqlPermission(gqlPermissionLoggedIn);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { category } from "./category";
|
|
3
|
+
import { gqlPermissionManager, permissionManager } from "./common/permission";
|
|
4
|
+
|
|
5
|
+
export const product = db
|
|
6
|
+
.type("Product", {
|
|
7
|
+
name: db.string().description("Name of the product"),
|
|
8
|
+
description: db
|
|
9
|
+
.string({ optional: true })
|
|
10
|
+
.description("Description of the product"),
|
|
11
|
+
categoryId: db
|
|
12
|
+
.uuid()
|
|
13
|
+
.description("ID of the category the product belongs to")
|
|
14
|
+
.relation({ type: "n-1", toward: { type: category } }),
|
|
15
|
+
...db.fields.timestamps(),
|
|
16
|
+
})
|
|
17
|
+
.permission(permissionManager)
|
|
18
|
+
.gqlPermission(gqlPermissionManager);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
import { gqlPermissionManager, permissionManager } from "./common/permission";
|
|
3
|
+
|
|
4
|
+
export const user = db
|
|
5
|
+
.type("User", {
|
|
6
|
+
name: db.string().description("Name of the user"),
|
|
7
|
+
email: db.string().unique().description("Email address of the user"),
|
|
8
|
+
role: db.enum("MANAGER", "STAFF"),
|
|
9
|
+
...db.fields.timestamps(),
|
|
10
|
+
})
|
|
11
|
+
.permission(permissionManager)
|
|
12
|
+
.gqlPermission(gqlPermissionManager);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createExecutor, recordUpdatedTrigger } from "@tailor-platform/sdk";
|
|
2
|
+
import { inventory } from "../db/inventory";
|
|
3
|
+
import config from "../../tailor.config";
|
|
4
|
+
import { getDB } from "../generated/kysely-tailordb";
|
|
5
|
+
|
|
6
|
+
export default createExecutor({
|
|
7
|
+
name: "check-inventory",
|
|
8
|
+
description: "Notify when inventory drops below threshold",
|
|
9
|
+
trigger: recordUpdatedTrigger({
|
|
10
|
+
type: inventory,
|
|
11
|
+
condition: ({ oldRecord, newRecord }) =>
|
|
12
|
+
oldRecord.quantity >= 10 && newRecord.quantity < 10,
|
|
13
|
+
}),
|
|
14
|
+
operation: {
|
|
15
|
+
kind: "function",
|
|
16
|
+
body: async ({ newRecord }) => {
|
|
17
|
+
const db = getDB("main-db");
|
|
18
|
+
|
|
19
|
+
await db
|
|
20
|
+
.insertInto("Notification")
|
|
21
|
+
.values({
|
|
22
|
+
message: `Inventory for product ${newRecord.productId} is below threshold. Current quantity: ${newRecord.quantity}`,
|
|
23
|
+
})
|
|
24
|
+
.execute();
|
|
25
|
+
},
|
|
26
|
+
invoker: config.auth.invoker("manager"),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type ColumnType, Kysely } from "kysely";
|
|
2
|
+
import { TailordbDialect } from "@tailor-platform/function-kysely-tailordb";
|
|
3
|
+
|
|
4
|
+
type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
|
5
|
+
type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
|
6
|
+
? ColumnType<S, I | undefined, U>
|
|
7
|
+
: ColumnType<T, T | undefined, T>;
|
|
8
|
+
type Serial<T = string | number> = ColumnType<T, never, never>;
|
|
9
|
+
|
|
10
|
+
export interface Namespace {
|
|
11
|
+
"main-db": {
|
|
12
|
+
Category: {
|
|
13
|
+
id: Generated<string>;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
createdAt: Generated<Timestamp>;
|
|
17
|
+
updatedAt: Timestamp | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Contact: {
|
|
21
|
+
id: Generated<string>;
|
|
22
|
+
name: string;
|
|
23
|
+
email: string;
|
|
24
|
+
phone: string | null;
|
|
25
|
+
address: string | null;
|
|
26
|
+
createdAt: Generated<Timestamp>;
|
|
27
|
+
updatedAt: Timestamp | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Inventory: {
|
|
31
|
+
id: Generated<string>;
|
|
32
|
+
productId: string;
|
|
33
|
+
quantity: number;
|
|
34
|
+
createdAt: Generated<Timestamp>;
|
|
35
|
+
updatedAt: Timestamp | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Notification: {
|
|
39
|
+
id: Generated<string>;
|
|
40
|
+
message: string;
|
|
41
|
+
createdAt: Generated<Timestamp>;
|
|
42
|
+
updatedAt: Timestamp | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Order: {
|
|
46
|
+
id: Generated<string>;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string | null;
|
|
49
|
+
orderDate: Timestamp;
|
|
50
|
+
orderType: "PURCHASE" | "SALES";
|
|
51
|
+
contactId: string;
|
|
52
|
+
createdAt: Generated<Timestamp>;
|
|
53
|
+
updatedAt: Timestamp | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
OrderItem: {
|
|
57
|
+
id: Generated<string>;
|
|
58
|
+
orderId: string;
|
|
59
|
+
productId: string;
|
|
60
|
+
quantity: number;
|
|
61
|
+
unitPrice: number;
|
|
62
|
+
totalPrice: Generated<number | null>;
|
|
63
|
+
createdAt: Generated<Timestamp>;
|
|
64
|
+
updatedAt: Timestamp | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Product: {
|
|
68
|
+
id: Generated<string>;
|
|
69
|
+
name: string;
|
|
70
|
+
description: string | null;
|
|
71
|
+
categoryId: string;
|
|
72
|
+
createdAt: Generated<Timestamp>;
|
|
73
|
+
updatedAt: Timestamp | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
User: {
|
|
77
|
+
id: Generated<string>;
|
|
78
|
+
name: string;
|
|
79
|
+
email: string;
|
|
80
|
+
role: "MANAGER" | "STAFF";
|
|
81
|
+
createdAt: Generated<Timestamp>;
|
|
82
|
+
updatedAt: Timestamp | null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getDB<const N extends keyof Namespace>(namespace: N): Kysely<Namespace[N]> {
|
|
88
|
+
const client = new tailordb.Client({ namespace });
|
|
89
|
+
return new Kysely<Namespace[N]>({ dialect: new TailordbDialect(client) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type DB<N extends keyof Namespace = keyof Namespace> = ReturnType<typeof getDB<N>>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createResolver, t } from "@tailor-platform/sdk";
|
|
2
|
+
import { order } from "../db/order";
|
|
3
|
+
import { orderItem } from "../db/orderItem";
|
|
4
|
+
import { type DB, getDB } from "../generated/kysely-tailordb";
|
|
5
|
+
|
|
6
|
+
const input = {
|
|
7
|
+
order: t.object(order.omitFields(["id", "createdAt"])),
|
|
8
|
+
items: t.object(orderItem.omitFields(["id", "createdAt"]), { array: true }),
|
|
9
|
+
};
|
|
10
|
+
interface Input {
|
|
11
|
+
order: t.infer<typeof input.order>;
|
|
12
|
+
items: t.infer<typeof input.items>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const insertOrder = async (db: DB<"main-db">, input: Input) => {
|
|
16
|
+
// Insert Order
|
|
17
|
+
const order = await db
|
|
18
|
+
.insertInto("Order")
|
|
19
|
+
.values(input.order)
|
|
20
|
+
.returning("id")
|
|
21
|
+
.executeTakeFirstOrThrow();
|
|
22
|
+
|
|
23
|
+
// Insert OrderItems
|
|
24
|
+
await db
|
|
25
|
+
.insertInto("OrderItem")
|
|
26
|
+
.values(
|
|
27
|
+
input.items.map((item) => ({
|
|
28
|
+
...item,
|
|
29
|
+
orderId: order.id,
|
|
30
|
+
})),
|
|
31
|
+
)
|
|
32
|
+
.execute();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const updateInventory = async (db: DB<"main-db">, input: Input) => {
|
|
36
|
+
for (const item of input.items) {
|
|
37
|
+
const inventory = await db
|
|
38
|
+
.selectFrom("Inventory")
|
|
39
|
+
.selectAll()
|
|
40
|
+
.where("productId", "=", item.productId)
|
|
41
|
+
.forUpdate()
|
|
42
|
+
.executeTakeFirst();
|
|
43
|
+
|
|
44
|
+
// If inventory already exists, update it.
|
|
45
|
+
// Otherwise, create it (only for PURCHASE order).
|
|
46
|
+
if (inventory) {
|
|
47
|
+
let quantity: number;
|
|
48
|
+
if (input.order.orderType === "PURCHASE") {
|
|
49
|
+
quantity = inventory.quantity + item.quantity;
|
|
50
|
+
} else {
|
|
51
|
+
quantity = inventory.quantity - item.quantity;
|
|
52
|
+
}
|
|
53
|
+
if (quantity < 0) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Cannot create order because inventory is not enough. productId: ${item.productId}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
await db
|
|
59
|
+
.updateTable("Inventory")
|
|
60
|
+
.set({ quantity })
|
|
61
|
+
.where("id", "=", inventory.id)
|
|
62
|
+
.execute();
|
|
63
|
+
} else {
|
|
64
|
+
if (input.order.orderType === "PURCHASE") {
|
|
65
|
+
await db
|
|
66
|
+
.insertInto("Inventory")
|
|
67
|
+
.values({
|
|
68
|
+
productId: item.productId,
|
|
69
|
+
quantity: item.quantity,
|
|
70
|
+
})
|
|
71
|
+
.execute();
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Cannot create order because inventory is not enough. productId: ${item.productId}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default createResolver({
|
|
82
|
+
name: "registerOrder",
|
|
83
|
+
operation: "mutation",
|
|
84
|
+
input,
|
|
85
|
+
body: async (context) => {
|
|
86
|
+
const db = getDB("main-db");
|
|
87
|
+
await db.transaction().execute(async (trx) => {
|
|
88
|
+
await insertOrder(trx, context.input);
|
|
89
|
+
await updateInventory(trx, context.input);
|
|
90
|
+
});
|
|
91
|
+
return { success: true };
|
|
92
|
+
},
|
|
93
|
+
output: t
|
|
94
|
+
.object({
|
|
95
|
+
success: t
|
|
96
|
+
.bool()
|
|
97
|
+
.description("Whether the order was registered successfully"),
|
|
98
|
+
})
|
|
99
|
+
.description("Result of order registration"),
|
|
100
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAuth,
|
|
3
|
+
defineConfig,
|
|
4
|
+
defineGenerators,
|
|
5
|
+
} from "@tailor-platform/sdk";
|
|
6
|
+
import { user } from "./src/db/user";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
name: "inventory-management",
|
|
10
|
+
db: { "main-db": { files: [`./src/db/*.ts`] } },
|
|
11
|
+
resolver: { "main-resolver": { files: [`./src/pipeline/*.ts`] } },
|
|
12
|
+
auth: defineAuth("main-auth", {
|
|
13
|
+
userProfile: {
|
|
14
|
+
type: user,
|
|
15
|
+
usernameField: "email",
|
|
16
|
+
attributes: {
|
|
17
|
+
role: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
machineUsers: {
|
|
21
|
+
manager: {
|
|
22
|
+
attributes: { role: "MANAGER" },
|
|
23
|
+
},
|
|
24
|
+
staff: {
|
|
25
|
+
attributes: { role: "STAFF" },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
executor: { files: ["./src/executor/*.ts"] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const generators = defineGenerators([
|
|
33
|
+
"@tailor-platform/kysely-type",
|
|
34
|
+
{ distPath: `./src/generated/kysely-tailordb.ts` },
|
|
35
|
+
]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["node", "@tailor-platform/function-types"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "tailor.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide covers testing patterns for Tailor Platform SDK applications using [Vitest](https://vitest.dev/).
|
|
4
|
+
|
|
5
|
+
This project was bootstrapped with [Create Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/create-sdk).
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Unit Tests
|
|
10
|
+
|
|
11
|
+
Run unit tests locally without deployment:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run test:unit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### End-to-End (E2E) Tests
|
|
18
|
+
|
|
19
|
+
E2E tests require a deployed application.
|
|
20
|
+
|
|
21
|
+
#### Local Development
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. Login
|
|
25
|
+
npx tailor-sdk login
|
|
26
|
+
|
|
27
|
+
# 2. Create workspace
|
|
28
|
+
npx tailor-sdk workspace create --name <workspace-name> --region <workspace-region>
|
|
29
|
+
|
|
30
|
+
# 3. Deploy application
|
|
31
|
+
export TAILOR_PLATFORM_WORKSPACE_ID=<your-workspace-id>
|
|
32
|
+
npm run deploy
|
|
33
|
+
|
|
34
|
+
# 4. Run E2E tests
|
|
35
|
+
npm run test:e2e
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### CI/CD (Automated Workspace Lifecycle)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx tailor-sdk login
|
|
42
|
+
export CI=true
|
|
43
|
+
export TAILOR_PLATFORM_WORKSPACE_NAME=<workspace-name>
|
|
44
|
+
export TAILOR_PLATFORM_WORKSPACE_REGION=<workspace-region>
|
|
45
|
+
npm run test:e2e # Automatically creates, deploys, tests, and deletes workspace
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Testing Patterns
|
|
49
|
+
|
|
50
|
+
### Unit Tests
|
|
51
|
+
|
|
52
|
+
Unit tests verify resolver logic without requiring deployment.
|
|
53
|
+
|
|
54
|
+
#### Simple Resolver Testing
|
|
55
|
+
|
|
56
|
+
**Example:** [src/resolver/simple.test.ts](src/resolver/simple.test.ts)
|
|
57
|
+
|
|
58
|
+
Test resolvers by directly calling `resolver.body()` with mock inputs.
|
|
59
|
+
|
|
60
|
+
**Key points:**
|
|
61
|
+
|
|
62
|
+
- Use `unauthenticatedTailorUser` for testing logic that doesn't depend on user context
|
|
63
|
+
- **Best for:** Calculations, data transformations without database dependencies
|
|
64
|
+
|
|
65
|
+
#### Mock TailorDB Client
|
|
66
|
+
|
|
67
|
+
**Example:** [src/resolver/mockTailordb.test.ts](src/resolver/mockTailordb.test.ts)
|
|
68
|
+
|
|
69
|
+
Mock the global `tailordb.Client` using `vi.stubGlobal()` to simulate database operations and control responses for each query.
|
|
70
|
+
|
|
71
|
+
**Key points:**
|
|
72
|
+
|
|
73
|
+
- Control exact database responses (query results, errors)
|
|
74
|
+
- Verify database interaction flow (transactions, queries)
|
|
75
|
+
- Test transaction rollback scenarios
|
|
76
|
+
- **Best for:** Business logic with simple database operations
|
|
77
|
+
|
|
78
|
+
#### Dependency Injection Pattern
|
|
79
|
+
|
|
80
|
+
**Example:** [src/resolver/wrapTailordb.test.ts](src/resolver/wrapTailordb.test.ts)
|
|
81
|
+
|
|
82
|
+
Extract database operations into a `DbOperations` interface, allowing business logic to be tested independently from Kysely implementation.
|
|
83
|
+
|
|
84
|
+
**Key points:**
|
|
85
|
+
|
|
86
|
+
- Test business logic independently from Kysely implementation details
|
|
87
|
+
- Mock high-level operations instead of low-level SQL queries
|
|
88
|
+
- **Best for:** Complex business logic with multiple database operations
|
|
89
|
+
|
|
90
|
+
### End-to-End (E2E) Tests
|
|
91
|
+
|
|
92
|
+
E2E tests verify your application works correctly when deployed to Tailor Platform. They test the full stack including GraphQL API, database operations, and authentication.
|
|
93
|
+
|
|
94
|
+
**Examples:** [e2e/resolver.test.ts](e2e/resolver.test.ts), [e2e/globalSetup.ts](e2e/globalSetup.ts)
|
|
95
|
+
|
|
96
|
+
#### How It Works
|
|
97
|
+
|
|
98
|
+
**1. Global Setup** ([e2e/globalSetup.ts](e2e/globalSetup.ts))
|
|
99
|
+
|
|
100
|
+
Before running tests, `globalSetup` retrieves deployment information:
|
|
101
|
+
|
|
102
|
+
- Application URL via `show()`
|
|
103
|
+
- Machine user access token via `machineUserToken()`
|
|
104
|
+
- Provides credentials to tests via `inject("url")` and `inject("token")`
|
|
105
|
+
|
|
106
|
+
**2. Test Files** ([e2e/resolver.test.ts](e2e/resolver.test.ts))
|
|
107
|
+
|
|
108
|
+
Tests create a GraphQL client using injected credentials and send real queries/mutations to the deployed application.
|
|
109
|
+
|
|
110
|
+
**Key points:**
|
|
111
|
+
|
|
112
|
+
- Tests run against actual deployed application
|
|
113
|
+
- `inject("url")` and `inject("token")` provide deployment credentials automatically
|
|
114
|
+
- Machine user authentication enables API access without manual token management
|
|
115
|
+
- Verify database persistence and API contracts
|
|
116
|
+
- **Best for:** Integration testing, end-to-end API validation
|
|
117
|
+
|
|
118
|
+
## Available Scripts
|
|
119
|
+
|
|
120
|
+
| Script | Description |
|
|
121
|
+
| -------------- | -------------------------------------------- |
|
|
122
|
+
| `generate` | Generate TypeScript types from configuration |
|
|
123
|
+
| `deploy` | Deploy application to Tailor Platform |
|
|
124
|
+
| `test:unit` | Run unit tests locally |
|
|
125
|
+
| `test:e2e` | Run E2E tests against deployed application |
|
|
126
|
+
| `format` | Format code with Prettier |
|
|
127
|
+
| `format:check` | Check code formatting |
|
|
128
|
+
| `lint` | Lint code with ESLint |
|
|
129
|
+
| `lint:fix` | Fix linting issues automatically |
|
|
130
|
+
| `typecheck` | Run TypeScript type checking |
|