@tailor-platform/create-sdk 0.0.1 → 0.8.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/CHANGELOG.md +446 -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,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
apply,
|
|
3
|
+
machineUserToken,
|
|
4
|
+
show,
|
|
5
|
+
workspaceCreate,
|
|
6
|
+
workspaceDelete,
|
|
7
|
+
type WorkspaceInfo,
|
|
8
|
+
} from "@tailor-platform/sdk/cli";
|
|
9
|
+
import type { TestProject } from "vitest/node";
|
|
10
|
+
|
|
11
|
+
declare module "vitest" {
|
|
12
|
+
export interface ProvidedContext {
|
|
13
|
+
url: string;
|
|
14
|
+
token: string;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let createdWorkspace: WorkspaceInfo | null = null;
|
|
19
|
+
|
|
20
|
+
async function createWorkspace(name: string, region: string) {
|
|
21
|
+
console.log(`Creating workspace "${name}" in region "${region}"...`);
|
|
22
|
+
const workspace = await workspaceCreate({ name, region });
|
|
23
|
+
console.log(`Workspace "${workspace.name}" created successfully.`);
|
|
24
|
+
return workspace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function deployApplication() {
|
|
28
|
+
console.log("Deploying application...");
|
|
29
|
+
await apply();
|
|
30
|
+
console.log("Application deployed successfully.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function deleteWorkspace(workspaceId: string) {
|
|
34
|
+
console.log("Deleting workspace...");
|
|
35
|
+
await workspaceDelete({ workspaceId });
|
|
36
|
+
console.log("Workspace deleted successfully.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function setup(project: TestProject) {
|
|
40
|
+
const isCI = process.env.CI === "true";
|
|
41
|
+
if (isCI) {
|
|
42
|
+
const workspaceName = process.env.TAILOR_PLATFORM_WORKSPACE_NAME;
|
|
43
|
+
const workspaceRegion = process.env.TAILOR_PLATFORM_WORKSPACE_REGION;
|
|
44
|
+
if (!workspaceName || !workspaceRegion) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"TAILOR_PLATFORM_WORKSPACE_NAME and TAILOR_PLATFORM_WORKSPACE_REGION must be set when CI=true",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
createdWorkspace = await createWorkspace(workspaceName, workspaceRegion);
|
|
50
|
+
process.env.TAILOR_PLATFORM_WORKSPACE_ID = createdWorkspace.id;
|
|
51
|
+
await deployApplication();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const app = await show();
|
|
55
|
+
const tokens = await machineUserToken({ name: "admin" });
|
|
56
|
+
project.provide("url", app.url);
|
|
57
|
+
project.provide("token", tokens.accessToken);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function teardown() {
|
|
61
|
+
if (createdWorkspace) {
|
|
62
|
+
await deleteWorkspace(createdWorkspace.id);
|
|
63
|
+
createdWorkspace = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { gql, GraphQLClient } from "graphql-request";
|
|
3
|
+
import { describe, expect, inject, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
function createGraphQLClient() {
|
|
6
|
+
const endpoint = new URL("/query", inject("url")).href;
|
|
7
|
+
return new GraphQLClient(endpoint, {
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${inject("token")}`,
|
|
10
|
+
},
|
|
11
|
+
// Prevent throwing errors on GraphQL errors.
|
|
12
|
+
errorPolicy: "all",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("resolver", () => {
|
|
17
|
+
const graphQLClient = createGraphQLClient();
|
|
18
|
+
|
|
19
|
+
describe("incrementUserAge", () => {
|
|
20
|
+
const uuid = randomUUID();
|
|
21
|
+
|
|
22
|
+
test("prepare data", async () => {
|
|
23
|
+
const query = gql`
|
|
24
|
+
mutation {
|
|
25
|
+
createUser(input: {
|
|
26
|
+
name: "alice"
|
|
27
|
+
email: "alice-${uuid}@example.com"
|
|
28
|
+
age: 30
|
|
29
|
+
}) {
|
|
30
|
+
id
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
const result = await graphQLClient.rawRequest(query);
|
|
35
|
+
expect(result.errors).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("basic functionality", async () => {
|
|
39
|
+
const query = gql`
|
|
40
|
+
mutation {
|
|
41
|
+
incrementUserAge(email: "alice-${uuid}@example.com") {
|
|
42
|
+
oldAge
|
|
43
|
+
newAge
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
const result = await graphQLClient.rawRequest(query);
|
|
48
|
+
expect(result.errors).toBeUndefined();
|
|
49
|
+
expect(result.data).toEqual({
|
|
50
|
+
incrementUserAge: {
|
|
51
|
+
oldAge: 30,
|
|
52
|
+
newAge: 31,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import eslint from "@eslint/js";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
3
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
// Ignore sdk's output directory.
|
|
7
|
+
globalIgnores([".tailor-sdk/", "src/generated/"]),
|
|
8
|
+
// Use recommended rules.
|
|
9
|
+
// https://typescript-eslint.io/users/configs#projects-with-type-checking
|
|
10
|
+
eslint.configs.recommended,
|
|
11
|
+
tseslint.configs.recommendedTypeChecked,
|
|
12
|
+
tseslint.configs.stylisticTypeChecked,
|
|
13
|
+
{
|
|
14
|
+
languageOptions: {
|
|
15
|
+
parserOptions: {
|
|
16
|
+
projectService: true,
|
|
17
|
+
tsconfigRootDir: import.meta.dirname,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// Disable type-checked linting for root config files.
|
|
22
|
+
// https://typescript-eslint.io/troubleshooting/typed-linting/#how-do-i-disable-type-checked-linting-for-a-file
|
|
23
|
+
{
|
|
24
|
+
files: ["eslint.config.js"],
|
|
25
|
+
extends: [tseslint.configs.disableTypeChecked],
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "testing",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"generate": "tailor-sdk generate",
|
|
7
|
+
"deploy": "tailor-sdk apply",
|
|
8
|
+
"test": "vitest --project unit",
|
|
9
|
+
"test:unit": "vitest --project unit",
|
|
10
|
+
"test:e2e": "vitest --project e2e",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"format:check": "prettier --check .",
|
|
13
|
+
"lint": "eslint --cache .",
|
|
14
|
+
"lint:fix": "eslint --cache --fix .",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@tailor-platform/function-kysely-tailordb": "0.1.3",
|
|
19
|
+
"kysely": "0.28.7"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@eslint/js": "9.39.1",
|
|
23
|
+
"@tailor-platform/function-types": "0.7.2",
|
|
24
|
+
"@tailor-platform/sdk": "0.8.0",
|
|
25
|
+
"@types/node": "22.19.0",
|
|
26
|
+
"eslint": "9.39.1",
|
|
27
|
+
"graphql-request": "7.3.1",
|
|
28
|
+
"prettier": "3.6.2",
|
|
29
|
+
"typescript": "5.9.3",
|
|
30
|
+
"typescript-eslint": "8.46.3",
|
|
31
|
+
"vitest": "4.0.8"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { db } from "@tailor-platform/sdk";
|
|
2
|
+
|
|
3
|
+
export const user = db
|
|
4
|
+
.type("User", {
|
|
5
|
+
name: db.string(),
|
|
6
|
+
email: db.string().unique(),
|
|
7
|
+
age: db.int(),
|
|
8
|
+
...db.fields.timestamps(),
|
|
9
|
+
})
|
|
10
|
+
.permission({
|
|
11
|
+
create: [[{ user: "_loggedIn" }, "=", true]],
|
|
12
|
+
read: [[{ user: "_loggedIn" }, "=", true]],
|
|
13
|
+
update: [[{ user: "_loggedIn" }, "=", true]],
|
|
14
|
+
delete: [[{ user: "_loggedIn" }, "=", true]],
|
|
15
|
+
})
|
|
16
|
+
.gqlPermission([
|
|
17
|
+
{
|
|
18
|
+
conditions: [[{ user: "_loggedIn" }, "=", true]],
|
|
19
|
+
actions: "all",
|
|
20
|
+
permit: true,
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
User: {
|
|
13
|
+
id: Generated<string>;
|
|
14
|
+
name: string;
|
|
15
|
+
email: string;
|
|
16
|
+
age: number;
|
|
17
|
+
createdAt: Generated<Timestamp>;
|
|
18
|
+
updatedAt: Timestamp | null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDB<const N extends keyof Namespace>(namespace: N): Kysely<Namespace[N]> {
|
|
24
|
+
const client = new tailordb.Client({ namespace });
|
|
25
|
+
return new Kysely<Namespace[N]>({ dialect: new TailordbDialect(client) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type DB<N extends keyof Namespace = keyof Namespace> = ReturnType<typeof getDB<N>>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
|
|
2
|
+
import {
|
|
3
|
+
afterAll,
|
|
4
|
+
afterEach,
|
|
5
|
+
beforeAll,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
test,
|
|
9
|
+
vi,
|
|
10
|
+
} from "vitest";
|
|
11
|
+
import resolver from "./mockTailordb";
|
|
12
|
+
|
|
13
|
+
describe("incrementUserAge resolver", () => {
|
|
14
|
+
// Mock queryObject method to simulate database interactions
|
|
15
|
+
const mockQueryObject = vi.fn();
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
vi.stubGlobal("tailordb", {
|
|
18
|
+
Client: vi.fn(
|
|
19
|
+
class {
|
|
20
|
+
connect = vi.fn();
|
|
21
|
+
end = vi.fn();
|
|
22
|
+
queryObject = mockQueryObject;
|
|
23
|
+
},
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
mockQueryObject.mockReset();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("basic functionality", async () => {
|
|
35
|
+
// 1: Begin transaction
|
|
36
|
+
mockQueryObject.mockResolvedValueOnce({});
|
|
37
|
+
// 2: Select current age
|
|
38
|
+
mockQueryObject.mockResolvedValueOnce({
|
|
39
|
+
rows: [{ age: 30 }],
|
|
40
|
+
});
|
|
41
|
+
// 3: Update age
|
|
42
|
+
mockQueryObject.mockResolvedValueOnce({});
|
|
43
|
+
// 4: Commit transaction
|
|
44
|
+
mockQueryObject.mockResolvedValueOnce({});
|
|
45
|
+
|
|
46
|
+
const result = await resolver.body({
|
|
47
|
+
input: { email: "test@example.com" },
|
|
48
|
+
user: unauthenticatedTailorUser,
|
|
49
|
+
});
|
|
50
|
+
expect(result).toEqual({ oldAge: 30, newAge: 31 });
|
|
51
|
+
expect(mockQueryObject).toHaveBeenCalledTimes(4);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("user not found", async () => {
|
|
55
|
+
// 1: Begin transaction
|
|
56
|
+
mockQueryObject.mockResolvedValueOnce({});
|
|
57
|
+
// 2: Select current age (no rows returned)
|
|
58
|
+
mockQueryObject.mockResolvedValueOnce({
|
|
59
|
+
rows: [],
|
|
60
|
+
});
|
|
61
|
+
// 3: Rollback transaction
|
|
62
|
+
mockQueryObject.mockResolvedValueOnce({});
|
|
63
|
+
|
|
64
|
+
const result = resolver.body({
|
|
65
|
+
input: { email: "test@example.com" },
|
|
66
|
+
user: unauthenticatedTailorUser,
|
|
67
|
+
});
|
|
68
|
+
await expect(result).rejects.toThrowError();
|
|
69
|
+
expect(mockQueryObject).toHaveBeenCalledTimes(3);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createResolver, t } from "@tailor-platform/sdk";
|
|
2
|
+
import { getDB } from "../generated/db";
|
|
3
|
+
|
|
4
|
+
const resolver = createResolver({
|
|
5
|
+
name: "incrementUserAge",
|
|
6
|
+
operation: "mutation",
|
|
7
|
+
input: {
|
|
8
|
+
email: t.string(),
|
|
9
|
+
},
|
|
10
|
+
body: async (context) => {
|
|
11
|
+
// Initialize database client
|
|
12
|
+
const db = getDB("main-db");
|
|
13
|
+
|
|
14
|
+
return await db.transaction().execute(async (trx) => {
|
|
15
|
+
// Select current age
|
|
16
|
+
const { age } = await trx
|
|
17
|
+
.selectFrom("User")
|
|
18
|
+
.where("email", "=", context.input.email)
|
|
19
|
+
.select("age")
|
|
20
|
+
.forUpdate()
|
|
21
|
+
.executeTakeFirstOrThrow();
|
|
22
|
+
|
|
23
|
+
// Increase age by 1
|
|
24
|
+
const oldAge = age;
|
|
25
|
+
const newAge = age + 1;
|
|
26
|
+
|
|
27
|
+
// Update age in database
|
|
28
|
+
await trx
|
|
29
|
+
.updateTable("User")
|
|
30
|
+
.set({ age: newAge })
|
|
31
|
+
.where("email", "=", context.input.email)
|
|
32
|
+
.execute();
|
|
33
|
+
|
|
34
|
+
// Return old and new age
|
|
35
|
+
return { oldAge, newAge };
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
output: t.object({
|
|
39
|
+
oldAge: t.int(),
|
|
40
|
+
newAge: t.int(),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default resolver;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { unauthenticatedTailorUser } from "@tailor-platform/sdk";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import resolver from "./simple";
|
|
4
|
+
|
|
5
|
+
describe("add resolver", () => {
|
|
6
|
+
test("basic functionality", async () => {
|
|
7
|
+
const result = await resolver.body({
|
|
8
|
+
input: { left: 1, right: 2 },
|
|
9
|
+
user: unauthenticatedTailorUser,
|
|
10
|
+
});
|
|
11
|
+
expect(result).toBe(3);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createResolver, t } from "@tailor-platform/sdk";
|
|
2
|
+
|
|
3
|
+
const resolver = createResolver({
|
|
4
|
+
name: "add",
|
|
5
|
+
operation: "query",
|
|
6
|
+
input: {
|
|
7
|
+
left: t.int(),
|
|
8
|
+
right: t.int(),
|
|
9
|
+
},
|
|
10
|
+
body: (context) => {
|
|
11
|
+
return context.input.left + context.input.right;
|
|
12
|
+
},
|
|
13
|
+
output: t.int(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default resolver;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { DbOperations, decrementUserAge } from "./wrapTailordb";
|
|
3
|
+
|
|
4
|
+
describe("decrementUserAge resolver", () => {
|
|
5
|
+
test("basic functionality", async () => {
|
|
6
|
+
// Mock database operations
|
|
7
|
+
const dbOperations = {
|
|
8
|
+
transaction: vi.fn(
|
|
9
|
+
async (fn: (ops: DbOperations) => Promise<unknown>) =>
|
|
10
|
+
await fn(dbOperations),
|
|
11
|
+
),
|
|
12
|
+
getUser: vi
|
|
13
|
+
.fn()
|
|
14
|
+
.mockResolvedValue({ email: "test@example.com", age: 30 }),
|
|
15
|
+
updateUser: vi.fn(),
|
|
16
|
+
} as DbOperations;
|
|
17
|
+
|
|
18
|
+
const result = await decrementUserAge("test@example.com", dbOperations);
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual({ oldAge: 30, newAge: 29 });
|
|
21
|
+
expect(dbOperations.transaction).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(dbOperations.getUser).toHaveBeenCalledExactlyOnceWith(
|
|
23
|
+
"test@example.com",
|
|
24
|
+
true,
|
|
25
|
+
);
|
|
26
|
+
expect(dbOperations.updateUser).toHaveBeenCalledExactlyOnceWith(
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
age: 29,
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("user not found", async () => {
|
|
34
|
+
// Mock database operations
|
|
35
|
+
const dbOperations = {
|
|
36
|
+
transaction: vi.fn(
|
|
37
|
+
async (fn: (ops: DbOperations) => Promise<unknown>) =>
|
|
38
|
+
await fn(dbOperations),
|
|
39
|
+
),
|
|
40
|
+
getUser: vi.fn().mockRejectedValue(new Error("User not found")),
|
|
41
|
+
updateUser: vi.fn(),
|
|
42
|
+
} as DbOperations;
|
|
43
|
+
|
|
44
|
+
const result = decrementUserAge("test@example.com", dbOperations);
|
|
45
|
+
|
|
46
|
+
expect(dbOperations.transaction).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(dbOperations.getUser).toHaveBeenCalledExactlyOnceWith(
|
|
48
|
+
"test@example.com",
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
await expect(result).rejects.toThrowError();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createResolver, t } from "@tailor-platform/sdk";
|
|
2
|
+
import { Selectable } from "kysely";
|
|
3
|
+
import { getDB, type DB, type Namespace } from "../generated/db";
|
|
4
|
+
|
|
5
|
+
export interface DbOperations {
|
|
6
|
+
transaction: <T>(fn: (ops: DbOperations) => Promise<T>) => Promise<T>;
|
|
7
|
+
|
|
8
|
+
getUser: (
|
|
9
|
+
email: string,
|
|
10
|
+
forUpdate: boolean,
|
|
11
|
+
) => Promise<Selectable<Namespace["main-db"]["User"]>>;
|
|
12
|
+
updateUser: (user: Selectable<Namespace["main-db"]["User"]>) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createDbOperations(db: DB<"main-db">): DbOperations {
|
|
16
|
+
return {
|
|
17
|
+
transaction: async <T>(
|
|
18
|
+
fn: (ops: DbOperations) => Promise<T>,
|
|
19
|
+
): Promise<T> => {
|
|
20
|
+
return await db.transaction().execute(async (trx) => {
|
|
21
|
+
const dbOperations = createDbOperations(trx);
|
|
22
|
+
return await fn(dbOperations);
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
getUser: async (email: string, forUpdate: boolean) => {
|
|
27
|
+
let query = db.selectFrom("User").where("email", "=", email).selectAll();
|
|
28
|
+
if (forUpdate) {
|
|
29
|
+
query = query.forUpdate();
|
|
30
|
+
}
|
|
31
|
+
return await query.executeTakeFirstOrThrow();
|
|
32
|
+
},
|
|
33
|
+
updateUser: async (user: Selectable<Namespace["main-db"]["User"]>) => {
|
|
34
|
+
await db
|
|
35
|
+
.updateTable("User")
|
|
36
|
+
.set({ name: user.name, age: user.age })
|
|
37
|
+
.where("email", "=", user.email)
|
|
38
|
+
.execute();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function decrementUserAge(
|
|
44
|
+
email: string,
|
|
45
|
+
dbOperations: DbOperations,
|
|
46
|
+
) {
|
|
47
|
+
return await dbOperations.transaction(async (ops) => {
|
|
48
|
+
// Select user
|
|
49
|
+
const user = await ops.getUser(email, true);
|
|
50
|
+
|
|
51
|
+
// Decrease age by 1
|
|
52
|
+
const oldAge = user.age;
|
|
53
|
+
const newAge = user.age - 1;
|
|
54
|
+
|
|
55
|
+
// Update user
|
|
56
|
+
await ops.updateUser({ ...user, age: newAge });
|
|
57
|
+
|
|
58
|
+
// Return old and new age
|
|
59
|
+
return { oldAge, newAge };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default createResolver({
|
|
64
|
+
name: "decrementUserAge",
|
|
65
|
+
operation: "mutation",
|
|
66
|
+
input: {
|
|
67
|
+
email: t.string(),
|
|
68
|
+
},
|
|
69
|
+
body: async (context) => {
|
|
70
|
+
// Initialize database client
|
|
71
|
+
const db = getDB("main-db");
|
|
72
|
+
const dbOperations = createDbOperations(db);
|
|
73
|
+
|
|
74
|
+
return await decrementUserAge(context.input.email, dbOperations);
|
|
75
|
+
},
|
|
76
|
+
output: t.object({
|
|
77
|
+
oldAge: t.int(),
|
|
78
|
+
newAge: t.int(),
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAuth,
|
|
3
|
+
defineConfig,
|
|
4
|
+
defineGenerators,
|
|
5
|
+
} from "@tailor-platform/sdk";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
name: "testing",
|
|
9
|
+
auth: defineAuth("main-auth", {
|
|
10
|
+
machineUsers: {
|
|
11
|
+
admin: {
|
|
12
|
+
attributes: {},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
db: { "main-db": { files: ["./src/db/*.ts"] } },
|
|
17
|
+
resolver: { "main-resolver": { files: ["./src/resolver/*.ts"] } },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const generators = defineGenerators([
|
|
21
|
+
"@tailor-platform/kysely-type",
|
|
22
|
+
{ distPath: "./src/generated/db.ts" },
|
|
23
|
+
]);
|
|
@@ -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": ["**/*.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
watch: false,
|
|
6
|
+
projects: [
|
|
7
|
+
{
|
|
8
|
+
test: {
|
|
9
|
+
name: { label: "unit", color: "blue" },
|
|
10
|
+
include: ["src/**/*.test.ts"],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
test: {
|
|
15
|
+
name: { label: "e2e", color: "green" },
|
|
16
|
+
include: ["e2e/**/*.test.ts"],
|
|
17
|
+
globalSetup: "e2e/globalSetup.ts",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
});
|