@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +448 -0
  2. package/LICENSE +21 -0
  3. package/README.md +23 -33
  4. package/dist/index.js +180 -0
  5. package/package.json +41 -8
  6. package/templates/hello-world/.gitignore +3 -0
  7. package/templates/hello-world/.prettierignore +1 -0
  8. package/templates/hello-world/.prettierrc +1 -0
  9. package/templates/hello-world/README.md +59 -0
  10. package/templates/hello-world/eslint.config.js +27 -0
  11. package/templates/hello-world/package.json +22 -0
  12. package/templates/hello-world/src/resolvers/hello.ts +19 -0
  13. package/templates/hello-world/tailor.config.ts +6 -0
  14. package/templates/hello-world/tsconfig.json +15 -0
  15. package/templates/inventory-management/.gitignore +3 -0
  16. package/templates/inventory-management/.prettierignore +2 -0
  17. package/templates/inventory-management/.prettierrc +1 -0
  18. package/templates/inventory-management/README.md +246 -0
  19. package/templates/inventory-management/eslint.config.js +33 -0
  20. package/templates/inventory-management/package.json +28 -0
  21. package/templates/inventory-management/src/db/category.ts +13 -0
  22. package/templates/inventory-management/src/db/common/permission.ts +59 -0
  23. package/templates/inventory-management/src/db/contact.ts +17 -0
  24. package/templates/inventory-management/src/db/inventory.ts +18 -0
  25. package/templates/inventory-management/src/db/notification.ts +21 -0
  26. package/templates/inventory-management/src/db/order.ts +20 -0
  27. package/templates/inventory-management/src/db/orderItem.ts +36 -0
  28. package/templates/inventory-management/src/db/product.ts +18 -0
  29. package/templates/inventory-management/src/db/user.ts +12 -0
  30. package/templates/inventory-management/src/executor/checkInventory.ts +28 -0
  31. package/templates/inventory-management/src/generated/kysely-tailordb.ts +92 -0
  32. package/templates/inventory-management/src/pipeline/registerOrder.ts +100 -0
  33. package/templates/inventory-management/tailor.config.ts +35 -0
  34. package/templates/inventory-management/tsconfig.json +16 -0
  35. package/templates/testing/.gitignore +3 -0
  36. package/templates/testing/.prettierignore +2 -0
  37. package/templates/testing/.prettierrc +1 -0
  38. package/templates/testing/README.md +130 -0
  39. package/templates/testing/e2e/globalSetup.ts +65 -0
  40. package/templates/testing/e2e/resolver.test.ts +57 -0
  41. package/templates/testing/eslint.config.js +27 -0
  42. package/templates/testing/package.json +33 -0
  43. package/templates/testing/src/db/user.ts +22 -0
  44. package/templates/testing/src/generated/db.ts +28 -0
  45. package/templates/testing/src/resolver/mockTailordb.test.ts +71 -0
  46. package/templates/testing/src/resolver/mockTailordb.ts +44 -0
  47. package/templates/testing/src/resolver/simple.test.ts +13 -0
  48. package/templates/testing/src/resolver/simple.ts +16 -0
  49. package/templates/testing/src/resolver/wrapTailordb.test.ts +53 -0
  50. package/templates/testing/src/resolver/wrapTailordb.ts +80 -0
  51. package/templates/testing/tailor.config.ts +23 -0
  52. package/templates/testing/tsconfig.json +16 -0
  53. 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.1",
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
+ });