Initial commit
This commit is contained in:
15
lib/api-client-react/package.json
Normal file
15
lib/api-client-react/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@workspace/api-client-react",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
}
|
||||
}
|
||||
310
lib/api-client-react/src/custom-fetch.ts
Normal file
310
lib/api-client-react/src/custom-fetch.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
export type CustomFetchOptions = RequestInit & {
|
||||
responseType?: "json" | "text" | "blob" | "auto";
|
||||
};
|
||||
|
||||
export type ErrorType<T = unknown> = ApiError<T>;
|
||||
|
||||
export type BodyType<T> = T;
|
||||
|
||||
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
||||
const DEFAULT_JSON_ACCEPT = "application/json, application/problem+json";
|
||||
|
||||
function isRequest(input: RequestInfo | URL): input is Request {
|
||||
return typeof Request !== "undefined" && input instanceof Request;
|
||||
}
|
||||
|
||||
function resolveMethod(input: RequestInfo | URL, explicitMethod?: string): string {
|
||||
if (explicitMethod) return explicitMethod.toUpperCase();
|
||||
if (isRequest(input)) return input.method.toUpperCase();
|
||||
return "GET";
|
||||
}
|
||||
|
||||
// Use loose check for URL — some runtimes (e.g. React Native) polyfill URL
|
||||
// differently, so `instanceof URL` can fail.
|
||||
function isUrl(input: RequestInfo | URL): input is URL {
|
||||
return typeof URL !== "undefined" && input instanceof URL;
|
||||
}
|
||||
|
||||
function resolveUrl(input: RequestInfo | URL): string {
|
||||
if (typeof input === "string") return input;
|
||||
if (isUrl(input)) return input.toString();
|
||||
return input.url;
|
||||
}
|
||||
|
||||
function mergeHeaders(...sources: Array<HeadersInit | undefined>): Headers {
|
||||
const headers = new Headers();
|
||||
|
||||
for (const source of sources) {
|
||||
if (!source) continue;
|
||||
new Headers(source).forEach((value, key) => {
|
||||
headers.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function getMediaType(headers: Headers): string | null {
|
||||
const value = headers.get("content-type");
|
||||
return value ? value.split(";", 1)[0].trim().toLowerCase() : null;
|
||||
}
|
||||
|
||||
function isJsonMediaType(mediaType: string | null): boolean {
|
||||
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
|
||||
}
|
||||
|
||||
function isTextMediaType(mediaType: string | null): boolean {
|
||||
return Boolean(
|
||||
mediaType &&
|
||||
(mediaType.startsWith("text/") ||
|
||||
mediaType === "application/xml" ||
|
||||
mediaType === "text/xml" ||
|
||||
mediaType.endsWith("+xml") ||
|
||||
mediaType === "application/x-www-form-urlencoded"),
|
||||
);
|
||||
}
|
||||
|
||||
// Loose equality (`== null`) handles both `null` (browser) and `undefined`
|
||||
// (React Native, which doesn't implement ReadableStream body).
|
||||
function hasNoBody(response: Response, method: string): boolean {
|
||||
if (method === "HEAD") return true;
|
||||
if (NO_BODY_STATUS.has(response.status)) return true;
|
||||
if (response.headers.get("content-length") === "0") return true;
|
||||
if (response.body == null) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function stripBom(text: string): string {
|
||||
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
||||
}
|
||||
|
||||
function looksLikeJson(text: string): boolean {
|
||||
const trimmed = text.trimStart();
|
||||
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
||||
}
|
||||
|
||||
function getStringField(value: unknown, key: string): string | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
|
||||
const candidate = (value as Record<string, unknown>)[key];
|
||||
if (typeof candidate !== "string") return undefined;
|
||||
|
||||
const trimmed = candidate.trim();
|
||||
return trimmed === "" ? undefined : trimmed;
|
||||
}
|
||||
|
||||
function truncate(text: string, maxLength = 300): string {
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
|
||||
}
|
||||
|
||||
function buildErrorMessage(response: Response, data: unknown): string {
|
||||
const prefix = `HTTP ${response.status} ${response.statusText}`;
|
||||
|
||||
if (typeof data === "string") {
|
||||
const text = data.trim();
|
||||
return text ? `${prefix}: ${truncate(text)}` : prefix;
|
||||
}
|
||||
|
||||
const title = getStringField(data, "title");
|
||||
const detail = getStringField(data, "detail");
|
||||
const message =
|
||||
getStringField(data, "message") ??
|
||||
getStringField(data, "error_description") ??
|
||||
getStringField(data, "error");
|
||||
|
||||
if (title && detail) return `${prefix}: ${title} — ${detail}`;
|
||||
if (detail) return `${prefix}: ${detail}`;
|
||||
if (message) return `${prefix}: ${message}`;
|
||||
if (title) return `${prefix}: ${title}`;
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
export class ApiError<T = unknown> extends Error {
|
||||
readonly name = "ApiError";
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly data: T | null;
|
||||
readonly headers: Headers;
|
||||
readonly response: Response;
|
||||
readonly method: string;
|
||||
readonly url: string;
|
||||
|
||||
constructor(
|
||||
response: Response,
|
||||
data: T | null,
|
||||
requestInfo: { method: string; url: string },
|
||||
) {
|
||||
super(buildErrorMessage(response, data));
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.data = data;
|
||||
this.headers = response.headers;
|
||||
this.response = response;
|
||||
this.method = requestInfo.method;
|
||||
this.url = response.url || requestInfo.url;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseParseError extends Error {
|
||||
readonly name = "ResponseParseError";
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly headers: Headers;
|
||||
readonly response: Response;
|
||||
readonly method: string;
|
||||
readonly url: string;
|
||||
readonly rawBody: string;
|
||||
readonly cause: unknown;
|
||||
|
||||
constructor(
|
||||
response: Response,
|
||||
rawBody: string,
|
||||
cause: unknown,
|
||||
requestInfo: { method: string; url: string },
|
||||
) {
|
||||
super(
|
||||
`Failed to parse response from ${requestInfo.method} ${response.url || requestInfo.url} ` +
|
||||
`(${response.status} ${response.statusText}) as JSON`,
|
||||
);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.headers = response.headers;
|
||||
this.response = response;
|
||||
this.method = requestInfo.method;
|
||||
this.url = response.url || requestInfo.url;
|
||||
this.rawBody = rawBody;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
async function parseJsonBody(
|
||||
response: Response,
|
||||
requestInfo: { method: string; url: string },
|
||||
): Promise<unknown> {
|
||||
const raw = await response.text();
|
||||
const normalized = stripBom(raw);
|
||||
|
||||
if (normalized.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(normalized);
|
||||
} catch (cause) {
|
||||
throw new ResponseParseError(response, raw, cause, requestInfo);
|
||||
}
|
||||
}
|
||||
|
||||
async function parseErrorBody(response: Response, method: string): Promise<unknown> {
|
||||
if (hasNoBody(response, method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaType = getMediaType(response.headers);
|
||||
|
||||
// Fall back to text when blob() is unavailable (e.g. some React Native builds).
|
||||
if (mediaType && !isJsonMediaType(mediaType) && !isTextMediaType(mediaType)) {
|
||||
return typeof response.blob === "function" ? response.blob() : response.text();
|
||||
}
|
||||
|
||||
const raw = await response.text();
|
||||
const normalized = stripBom(raw);
|
||||
const trimmed = normalized.trim();
|
||||
|
||||
if (trimmed === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isJsonMediaType(mediaType) || looksLikeJson(normalized)) {
|
||||
try {
|
||||
return JSON.parse(normalized);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
function inferResponseType(response: Response): "json" | "text" | "blob" {
|
||||
const mediaType = getMediaType(response.headers);
|
||||
|
||||
if (isJsonMediaType(mediaType)) return "json";
|
||||
if (isTextMediaType(mediaType) || mediaType == null) return "text";
|
||||
return "blob";
|
||||
}
|
||||
|
||||
async function parseSuccessBody(
|
||||
response: Response,
|
||||
responseType: "json" | "text" | "blob" | "auto",
|
||||
requestInfo: { method: string; url: string },
|
||||
): Promise<unknown> {
|
||||
if (hasNoBody(response, requestInfo.method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveType =
|
||||
responseType === "auto" ? inferResponseType(response) : responseType;
|
||||
|
||||
switch (effectiveType) {
|
||||
case "json":
|
||||
return parseJsonBody(response, requestInfo);
|
||||
|
||||
case "text": {
|
||||
const text = await response.text();
|
||||
return text === "" ? null : text;
|
||||
}
|
||||
|
||||
case "blob":
|
||||
if (typeof response.blob !== "function") {
|
||||
throw new TypeError(
|
||||
"Blob responses are not supported in this runtime. " +
|
||||
"Use responseType \"json\" or \"text\" instead.",
|
||||
);
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
}
|
||||
|
||||
export async function customFetch<T = unknown>(
|
||||
input: RequestInfo | URL,
|
||||
options: CustomFetchOptions = {},
|
||||
): Promise<T> {
|
||||
const { responseType = "auto", headers: headersInit, ...init } = options;
|
||||
|
||||
const method = resolveMethod(input, init.method);
|
||||
|
||||
if (init.body != null && (method === "GET" || method === "HEAD")) {
|
||||
throw new TypeError(`customFetch: ${method} requests cannot have a body.`);
|
||||
}
|
||||
|
||||
const headers = mergeHeaders(isRequest(input) ? input.headers : undefined, headersInit);
|
||||
|
||||
if (
|
||||
typeof init.body === "string" &&
|
||||
!headers.has("content-type") &&
|
||||
looksLikeJson(init.body)
|
||||
) {
|
||||
headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
if (responseType === "json" && !headers.has("accept")) {
|
||||
headers.set("accept", DEFAULT_JSON_ACCEPT);
|
||||
}
|
||||
|
||||
const requestInfo = { method, url: resolveUrl(input) };
|
||||
|
||||
const response = await fetch(input, { ...init, method, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await parseErrorBody(response, method);
|
||||
throw new ApiError(response, errorData, requestInfo);
|
||||
}
|
||||
|
||||
return (await parseSuccessBody(response, responseType, requestInfo)) as T;
|
||||
}
|
||||
10
lib/api-client-react/src/generated/api.schemas.ts
Normal file
10
lib/api-client-react/src/generated/api.schemas.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
101
lib/api-client-react/src/generated/api.ts
Normal file
101
lib/api-client-react/src/generated/api.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type { HealthStatus } from "./api.schemas";
|
||||
|
||||
import { customFetch } from "../custom-fetch";
|
||||
import type { ErrorType } from "../custom-fetch";
|
||||
|
||||
type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
||||
|
||||
/**
|
||||
* Returns server health status
|
||||
* @summary Health check
|
||||
*/
|
||||
export const getHealthCheckUrl = () => {
|
||||
return `/api/healthz`;
|
||||
};
|
||||
|
||||
export const healthCheck = async (
|
||||
options?: RequestInit,
|
||||
): Promise<HealthStatus> => {
|
||||
return customFetch<HealthStatus>(getHealthCheckUrl(), {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const getHealthCheckQueryKey = () => {
|
||||
return [`/api/healthz`] as const;
|
||||
};
|
||||
|
||||
export const getHealthCheckQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof healthCheck>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof healthCheck>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getHealthCheckQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthCheck>>> = ({
|
||||
signal,
|
||||
}) => healthCheck({ signal, ...requestOptions });
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof healthCheck>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type HealthCheckQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof healthCheck>>
|
||||
>;
|
||||
export type HealthCheckQueryError = ErrorType<unknown>;
|
||||
|
||||
/**
|
||||
* @summary Health check
|
||||
*/
|
||||
|
||||
export function useHealthCheck<
|
||||
TData = Awaited<ReturnType<typeof healthCheck>>,
|
||||
TError = ErrorType<unknown>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof healthCheck>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getHealthCheckQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
2
lib/api-client-react/src/index.ts
Normal file
2
lib/api-client-react/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./generated/api";
|
||||
export * from "./generated/api.schemas";
|
||||
12
lib/api-client-react/tsconfig.json
Normal file
12
lib/api-client-react/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["dom", "es2022"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
36
lib/api-spec/openapi.yaml
Normal file
36
lib/api-spec/openapi.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
# Do not change the title, if the title changes, the import paths will be broken
|
||||
title: Api
|
||||
version: 0.1.0
|
||||
description: API specification
|
||||
servers:
|
||||
- url: /api
|
||||
description: Base API path
|
||||
tags:
|
||||
- name: health
|
||||
description: Health operations
|
||||
paths:
|
||||
/healthz:
|
||||
get:
|
||||
operationId: healthCheck
|
||||
tags: [health]
|
||||
summary: Health check
|
||||
description: Returns server health status
|
||||
responses:
|
||||
"200":
|
||||
description: Healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthStatus"
|
||||
components:
|
||||
schemas:
|
||||
HealthStatus:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
|
||||
69
lib/api-spec/orval.config.ts
Normal file
69
lib/api-spec/orval.config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineConfig, InputTransformerFn } from "orval";
|
||||
import path from "path";
|
||||
|
||||
const root = path.resolve(__dirname, "..", "..");
|
||||
const apiClientReactSrc = path.resolve(root, "lib", "api-client-react", "src");
|
||||
const apiZodSrc = path.resolve(root, "lib", "api-zod", "src");
|
||||
|
||||
// Our exports make assumptions about the title of the API being "Api" (i.e. generated output is `api.ts`).
|
||||
const titleTransformer: InputTransformerFn = (config) => {
|
||||
config.info ??= {};
|
||||
config.info.title = "Api";
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
"api-client-react": {
|
||||
input: {
|
||||
target: "./openapi.yaml",
|
||||
override: {
|
||||
transformer: titleTransformer,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
workspace: apiClientReactSrc,
|
||||
target: "generated",
|
||||
client: "react-query",
|
||||
mode: "split",
|
||||
baseUrl: "/api",
|
||||
clean: true,
|
||||
prettier: true,
|
||||
override: {
|
||||
fetch: {
|
||||
includeHttpResponseReturnType: false,
|
||||
},
|
||||
mutator: {
|
||||
path: path.resolve(apiClientReactSrc, "custom-fetch.ts"),
|
||||
name: "customFetch",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
zod: {
|
||||
input: {
|
||||
target: "./openapi.yaml",
|
||||
override: {
|
||||
transformer: titleTransformer,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
workspace: apiZodSrc,
|
||||
client: "zod",
|
||||
target: "generated",
|
||||
schemas: { path: "generated/types", type: "typescript" },
|
||||
mode: "split",
|
||||
clean: true,
|
||||
prettier: true,
|
||||
override: {
|
||||
zod: {
|
||||
coerce: {
|
||||
query: ['boolean', 'number', 'string'],
|
||||
param: ['boolean', 'number', 'string'],
|
||||
},
|
||||
},
|
||||
useDates: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
11
lib/api-spec/package.json
Normal file
11
lib/api-spec/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@workspace/api-spec",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "orval --config ./orval.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"orval": "^8.5.2"
|
||||
}
|
||||
}
|
||||
12
lib/api-zod/package.json
Normal file
12
lib/api-zod/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@workspace/api-zod",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
16
lib/api-zod/src/generated/api.ts
Normal file
16
lib/api-zod/src/generated/api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
import * as zod from "zod";
|
||||
|
||||
/**
|
||||
* Returns server health status
|
||||
* @summary Health check
|
||||
*/
|
||||
export const HealthCheckResponse = zod.object({
|
||||
status: zod.string(),
|
||||
});
|
||||
11
lib/api-zod/src/generated/types/healthStatus.ts
Normal file
11
lib/api-zod/src/generated/types/healthStatus.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export interface HealthStatus {
|
||||
status: string;
|
||||
}
|
||||
9
lib/api-zod/src/generated/types/index.ts
Normal file
9
lib/api-zod/src/generated/types/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Generated by orval v8.5.3 🍺
|
||||
* Do not edit manually.
|
||||
* Api
|
||||
* API specification
|
||||
* OpenAPI spec version: 0.1.0
|
||||
*/
|
||||
|
||||
export * from "./healthStatus";
|
||||
2
lib/api-zod/src/index.ts
Normal file
2
lib/api-zod/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./generated/api";
|
||||
export * from "./generated/types";
|
||||
11
lib/api-zod/tsconfig.json
Normal file
11
lib/api-zod/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
14
lib/db/drizzle.config.ts
Normal file
14
lib/db/drizzle.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL, ensure the database is provisioned");
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
schema: path.join(__dirname, "./src/schema/index.ts"),
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
});
|
||||
25
lib/db/package.json
Normal file
25
lib/db/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@workspace/db",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schema": "./src/schema/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"push": "drizzle-kit push --config ./drizzle.config.ts",
|
||||
"push-force": "drizzle-kit push --force --config ./drizzle.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/pg": "^8.18.0",
|
||||
"drizzle-kit": "^0.31.9"
|
||||
}
|
||||
}
|
||||
16
lib/db/src/index.ts
Normal file
16
lib/db/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import pg from "pg";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle(pool, { schema });
|
||||
|
||||
export * from "./schema";
|
||||
20
lib/db/src/schema/index.ts
Normal file
20
lib/db/src/schema/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Export your models here. Add one export per file
|
||||
// export * from "./posts";
|
||||
//
|
||||
// Each model/table should ideally be split into different files.
|
||||
// Each model/table should define a Drizzle table, insert schema, and types:
|
||||
//
|
||||
// import { pgTable, text, serial } from "drizzle-orm/pg-core";
|
||||
// import { createInsertSchema } from "drizzle-zod";
|
||||
// import { z } from "zod/v4";
|
||||
//
|
||||
// export const postsTable = pgTable("posts", {
|
||||
// id: serial("id").primaryKey(),
|
||||
// title: text("title").notNull(),
|
||||
// });
|
||||
//
|
||||
// export const insertPostSchema = createInsertSchema(postsTable).omit({ id: true });
|
||||
// export type InsertPost = z.infer<typeof insertPostSchema>;
|
||||
// export type Post = typeof postsTable.$inferSelect;
|
||||
|
||||
export {}
|
||||
12
lib/db/tsconfig.json
Normal file
12
lib/db/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user