diff --git a/AGENTS.md b/AGENTS.md index 8bad0e4..47f4706 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,7 +65,7 @@ import type { TaylorDatabase } from "./types.js"; export const queryBuilder = createQueryBuilder({ baseUrl: process.env.TAYLORDB_BASE_URL!, baseId: process.env.TAYLORDB_SERVER_ID!, - apiKey: process.env.TAYLORDB_API_TOKEN!, + apiKey: "", }); // ============================================================================ diff --git a/README.md b/README.md index fdf1026..96d2989 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,6 @@ This template is designed to deploy to TaylorDB's platform using the included `t **Environment Variables Required:** - `TAYLORDB_BASE_URL` -- `TAYLORDB_API_TOKEN` - `TAYLORDB_SERVER_ID` --- diff --git a/apps/server/index.ts b/apps/server/index.ts index 4849272..4b802c7 100644 --- a/apps/server/index.ts +++ b/apps/server/index.ts @@ -1,5 +1,6 @@ import express from "express"; import cors from "cors"; +import cookieParser from "cookie-parser"; import * as trpcExpress from "@trpc/server/adapters/express"; import { appRouter } from "./router.js"; import { createContext } from "./trpc.js"; @@ -7,6 +8,9 @@ import { createContext } from "./trpc.js"; const app = express(); const PORT = process.env.PORT || 3001; +// Parse cookies +app.use(cookieParser()); + // Enable CORS for frontend (adjust origin in production) app.use( cors({ diff --git a/apps/server/package.json b/apps/server/package.json index 8676f51..1fcd0e1 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -14,13 +14,15 @@ "start": "node dist/index.js" }, "dependencies": { - "@taylordb/query-builder": "^0.10.3", + "@taylordb/query-builder": "^0.11.4", "@trpc/server": "^11.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.2.1", "zod": "^4.3.5" }, "devDependencies": { + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^24.10.1", diff --git a/apps/server/routers/posts.ts b/apps/server/routers/posts.ts index b12106f..33b230e 100644 --- a/apps/server/routers/posts.ts +++ b/apps/server/routers/posts.ts @@ -6,6 +6,39 @@ import { router, publicProcedure } from "../trpc"; * * Another example sub-router showing a different domain. * Demonstrates relationships (author references users). + * + * Example using TaylorDB queryBuilder (available via ctx.queryBuilder): + * + * // Query all posts + * const posts = await ctx.queryBuilder.from("posts").select("*"); + * + * // Query with filters + * const publishedPosts = await ctx.queryBuilder + * .from("posts") + * .where({ published: true }) + * .select("*"); + * + * // Create a new post + * const newPost = await ctx.queryBuilder + * .from("posts") + * .insert({ + * title: "My Title", + * content: "My Content", + * authorId: 1, + * published: false + * }); + * + * // Update a post + * const updatedPost = await ctx.queryBuilder + * .from("posts") + * .where({ id: 1 }) + * .update({ published: true }); + * + * // Delete a post + * await ctx.queryBuilder + * .from("posts") + * .where({ id: 1 }) + * .delete(); */ // In-memory store for demonstration @@ -36,9 +69,9 @@ export const postsRouter = router({ published: z.boolean().optional(), authorId: z.number().optional(), }) - .optional() + .optional(), ) - .query(({ input }) => { + .query(({ input, ctx }) => { let result = posts; if (input?.published !== undefined) { @@ -88,7 +121,7 @@ export const postsRouter = router({ title: z.string().min(1).max(200).optional(), content: z.string().min(1).optional(), published: z.boolean().optional(), - }) + }), ) .mutation(({ input }) => { const post = posts.find((p) => p.id === input.id); diff --git a/apps/server/taylordb/query-builder.ts b/apps/server/taylordb/query-builder.ts deleted file mode 100644 index 8787b3a..0000000 --- a/apps/server/taylordb/query-builder.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { createQueryBuilder } from "@taylordb/query-builder"; -import type { TaylorDatabase } from "./types.js"; - -/** - * TaylorDB Query Builder Instance - * - * This is the main query builder instance configured with your TaylorDB credentials. - * Use this to perform all database operations in a type-safe manner. - */ -export const queryBuilder = createQueryBuilder({ - baseUrl: process.env.TAYLORDB_BASE_URL!, - baseId: process.env.TAYLORDB_SERVER_ID!, - apiKey: process.env.TAYLORDB_API_TOKEN!, -}); - -/** - * ============================================================================ - * Example Query Functions - * ============================================================================ - * - * Below are example patterns for common database operations. - * Replace these with your own functions based on your actual schema. - * - * For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md - */ - -// ============================================================================ -// READ Operations (Queries) -// ============================================================================ - -/** - * Example: Get all records from a table - * - * @example - * export async function getAllUsers() { - * return await queryBuilder - * .selectFrom("users") - * .select(["id", "name", "email", "createdAt"]) - * .orderBy("createdAt", "desc") - * .execute(); - * } - */ - -/** - * Example: Get a single record by ID - * - * @example - * export async function getUserById(id: number) { - * return await queryBuilder - * .selectFrom("users") - * .where("id", "=", id) - * .executeTakeFirst(); - * } - */ - -/** - * Example: Get records with filtering - * - * @example - * export async function getActiveUsers() { - * return await queryBuilder - * .selectFrom("users") - * .where("status", "=", "active") - * .orderBy("name", "asc") - * .execute(); - * } - */ - -/** - * Example: Get records with date range filtering - * - * @example - * export async function getRecordsInDateRange(startDate: string, endDate: string) { - * return await queryBuilder - * .selectFrom("records") - * .where("date", ">=", ["exactDay", startDate]) - * .where("date", "<=", ["exactDay", endDate]) - * .orderBy("date", "asc") - * .execute(); - * } - */ - -// ============================================================================ -// CREATE Operations (Insert) -// ============================================================================ - -/** - * Example: Insert a new record - * - * @example - * export async function createUser(data: { name: string; email: string }) { - * return await queryBuilder - * .insertInto("users") - * .values({ - * name: data.name, - * email: data.email, - * status: "active", - * }) - * .executeTakeFirst(); - * } - */ - -/** - * Example: Insert with single-select field - * - * Note: Single-select fields must be wrapped in an array - * - * @example - * export async function createTask(data: { title: string; priority: "low" | "medium" | "high" }) { - * return await queryBuilder - * .insertInto("tasks") - * .values({ - * title: data.title, - * priority: [data.priority], // Wrap in array for single-select - * }) - * .executeTakeFirst(); - * } - */ - -/** - * Example: Insert with computed fields - * - * @example - * export async function createOrder(data: { quantity: number; pricePerUnit: number }) { - * const totalPrice = data.quantity * data.pricePerUnit; - * - * return await queryBuilder - * .insertInto("orders") - * .values({ - * quantity: data.quantity, - * pricePerUnit: data.pricePerUnit, - * totalPrice: totalPrice, - * }) - * .executeTakeFirst(); - * } - */ - -// ============================================================================ -// UPDATE Operations -// ============================================================================ - -/** - * Example: Update a record - * - * @example - * export async function updateUser(id: number, data: { name?: string; email?: string }) { - * return await queryBuilder - * .update("users") - * .set(data) - * .where("id", "=", id) - * .execute(); - * } - */ - -/** - * Example: Update with conditional recalculation - * - * @example - * export async function updateOrder(id: number, data: { quantity?: number; pricePerUnit?: number }) { - * // Fetch current record to compute total - * const currentOrder = await queryBuilder - * .selectFrom("orders") - * .select(["quantity", "pricePerUnit"]) - * .where("id", "=", id) - * .executeTakeFirst(); - * - * if (!currentOrder) { - * throw new Error("Order not found"); - * } - * - * const newQuantity = data.quantity ?? currentOrder.quantity ?? 0; - * const newPrice = data.pricePerUnit ?? currentOrder.pricePerUnit ?? 0; - * const totalPrice = newQuantity * newPrice; - * - * return await queryBuilder - * .update("orders") - * .set({ - * ...data, - * totalPrice, - * }) - * .where("id", "=", id) - * .execute(); - * } - */ - -// ============================================================================ -// DELETE Operations -// ============================================================================ - -/** - * Example: Delete a single record - * - * @example - * export async function deleteUser(id: number) { - * return await queryBuilder - * .deleteFrom("users") - * .where("id", "=", id) - * .execute(); - * } - */ - -/** - * Example: Delete multiple records by IDs - * - * @example - * export async function deleteUsers(ids: number[]) { - * return await queryBuilder - * .deleteFrom("users") - * .where("id", "hasAnyOf", ids) - * .execute(); - * } - */ - -/** - * Example: Delete with condition - * - * @example - * export async function deleteInactiveUsers() { - * return await queryBuilder - * .deleteFrom("users") - * .where("status", "=", "inactive") - * .execute(); - * } - */ - -// ============================================================================ -// AGGREGATION Operations (Manual) -// ============================================================================ - -/** - * Example: Calculate statistics - * - * @example - * export async function getUserStats() { - * const users = await queryBuilder - * .selectFrom("users") - * .select(["age"]) - * .execute(); - * - * if (users.length === 0) { - * return { count: 0, average: null, min: null, max: null }; - * } - * - * const ages = users.map(u => u.age).filter((a): a is number => a !== undefined); - * - * return { - * count: ages.length, - * average: ages.reduce((a, b) => a + b, 0) / ages.length, - * min: Math.min(...ages), - * max: Math.max(...ages), - * }; - * } - */ - -/** - * Example: Sum totals for a date - * - * @example - * export async function getTotalSalesForDate(date: string) { - * const sales = await queryBuilder - * .selectFrom("sales") - * .select(["amount", "quantity"]) - * .where("date", "=", ["exactDay", date]) - * .execute(); - * - * return { - * totalAmount: sales.reduce((sum, s) => sum + (s.amount ?? 0), 0), - * totalQuantity: sales.reduce((sum, s) => sum + (s.quantity ?? 0), 0), - * }; - * } - */ - -/** - * ============================================================================ - * Query Builder Quick Reference - * ============================================================================ - * - * SELECT: - * - .selectFrom("tableName") - * - .select(["field1", "field2"]) - * - .execute() // Returns array - * - .executeTakeFirst() // Returns single record or undefined - * - * WHERE: - * - .where("field", "=", value) - * - .where("field", ">", value) - * - .where("field", "hasAnyOf", [value1, value2]) - * - .where("date", ">=", ["exactDay", "2024-01-01"]) - * - * ORDER BY: - * - .orderBy("field", "asc") - * - .orderBy("field", "desc") - * - * INSERT: - * - .insertInto("tableName") - * - .values({ field1: value1, field2: value2 }) - * - .executeTakeFirst() - * - * UPDATE: - * - .update("tableName") - * - .set({ field1: value1 }) - * - .where("id", "=", id) - * - .execute() - * - * DELETE: - * - .deleteFrom("tableName") - * - .where("id", "=", id) - * - .execute() - * - * Field Types: - * - Text: string - * - Number: number - * - Date: ["exactDay", "YYYY-MM-DD"] - * - Single Select: ["option"] - * - Multi Select: ["opt1", "opt2"] - * - Boolean: true/false - * - * For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md - */ diff --git a/apps/server/trpc.ts b/apps/server/trpc.ts index 4c1fade..99fc0d6 100644 --- a/apps/server/trpc.ts +++ b/apps/server/trpc.ts @@ -1,15 +1,30 @@ +import { createQueryBuilder } from "@taylordb/query-builder"; import { initTRPC } from "@trpc/server"; import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +import { TaylorDatabase } from "./taylordb/types"; /** * Create context for each tRPC request * This is where you can add user session, database clients, etc. */ export const createContext = ({ req, res }: CreateExpressContextOptions) => { + // Extract app_access_token from cookies + const appAccessToken = req.cookies?.app_access_token; + + if (!appAccessToken) { + throw new Error("Unauthorized: app_access_token cookie is required"); + } + + const queryBuilder = createQueryBuilder({ + baseUrl: process.env.TAYLORDB_BASE_URL!, + baseId: process.env.TAYLORDB_SERVER_ID!, + apiKey: appAccessToken, + }); + return { req, res, - // Add any shared context here (e.g., database client, user session) + queryBuilder, }; }; diff --git a/docs/TAYLORDB_QUERY_REFERENCE.md b/docs/TAYLORDB_QUERY_REFERENCE.md index 57392cd..b9d50db 100644 --- a/docs/TAYLORDB_QUERY_REFERENCE.md +++ b/docs/TAYLORDB_QUERY_REFERENCE.md @@ -29,7 +29,7 @@ import type { TaylorDatabase } from "./types.js"; export const queryBuilder = createQueryBuilder({ baseUrl: process.env.TAYLORDB_BASE_URL!, baseId: process.env.TAYLORDB_SERVER_ID!, - apiKey: process.env.TAYLORDB_API_TOKEN!, + apiKey: "", }); ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 597056a..33fbd3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,9 @@ importers: '@trpc/server': specifier: ^11.8.1 version: 11.8.1(typescript@5.9.3) + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 cors: specifier: ^2.8.5 version: 2.8.5 @@ -154,6 +157,9 @@ importers: specifier: ^4.3.5 version: 4.3.5 devDependencies: + '@types/cookie-parser': + specifier: ^1.4.10 + version: 1.4.10(@types/express@5.0.6) '@types/cors': specifier: ^2.8.19 version: 2.8.19 @@ -1315,6 +1321,11 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} @@ -1569,6 +1580,13 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -3638,6 +3656,10 @@ snapshots: dependencies: '@types/node': 24.10.1 + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + '@types/cors@2.8.19': dependencies: '@types/node': 24.10.1 @@ -3942,6 +3964,13 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} diff --git a/taylordb.yml b/taylordb.yml index 4b6d660..9b0d724 100644 --- a/taylordb.yml +++ b/taylordb.yml @@ -21,7 +21,6 @@ services: env: vars: TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL - TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID FRONTEND_URL: routing.client.url @@ -31,7 +30,6 @@ services: env: vars: TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL - TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID FRONTEND_URL: routing.client.url @@ -41,7 +39,6 @@ services: env: vars: TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL - TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID FRONTEND_URL: routing.client.url