mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat(www): Finish up on the onboarding (#210)
Merging this prematurely to make sure the team is on the same boat... like dang! We need to find a better way to do this. Plus it has become too big
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { Hono } from "hono";
|
||||
import { notPublic } from "./auth";
|
||||
import { Result } from "../common";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { User } from "@nestri/core/user/index";
|
||||
import { Team } from "@nestri/core/team/index";
|
||||
import { assertActor } from "@nestri/core/actor";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { ErrorResponses, Result } from "./common";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
export module AccountApi {
|
||||
export const route = new Hono()
|
||||
@@ -14,8 +15,8 @@ export module AccountApi {
|
||||
.get("/",
|
||||
describeRoute({
|
||||
tags: ["Account"],
|
||||
summary: "Retrieve the current user's details",
|
||||
description: "Returns the user's account details, plus the teams they have joined",
|
||||
summary: "Get user account",
|
||||
description: "Get the current user's account details",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
@@ -24,35 +25,36 @@ export module AccountApi {
|
||||
z.object({
|
||||
...User.Info.shape,
|
||||
teams: Team.Info.array(),
|
||||
}).openapi({
|
||||
description: "User account information",
|
||||
example: { ...Examples.User, teams: [Examples.Team] }
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Successfully retrieved account details"
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.object({ error: z.string() })),
|
||||
},
|
||||
},
|
||||
description: "This account does not exist",
|
||||
description: "User account details"
|
||||
},
|
||||
404: ErrorResponses[404]
|
||||
}
|
||||
}),
|
||||
async (c) => {
|
||||
const actor = assertActor("user");
|
||||
const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()])
|
||||
|
||||
if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404)
|
||||
if (!currentUser)
|
||||
throw new VisibleError(
|
||||
"not_found",
|
||||
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||
"User not found",
|
||||
);
|
||||
|
||||
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
id,
|
||||
email,
|
||||
name,
|
||||
email,
|
||||
teams,
|
||||
avatarUrl,
|
||||
discriminator,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Resource } from "sst";
|
||||
import { subjects } from "../subjects";
|
||||
import { type MiddlewareHandler } from "hono";
|
||||
// import { User } from "@nestri/core/user/index";
|
||||
import { VisibleError } from "@nestri/core/error";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { useActor, withActor } from "@nestri/core/actor";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
const client = createClient({
|
||||
issuer: Resource.Urls.auth,
|
||||
@@ -15,7 +13,11 @@ const client = createClient({
|
||||
export const notPublic: MiddlewareHandler = async (c, next) => {
|
||||
const actor = useActor();
|
||||
if (actor.type === "public")
|
||||
throw new HTTPException(401, { message: "Unauthorized" });
|
||||
throw new VisibleError(
|
||||
"authentication",
|
||||
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||
"Missing authorization header",
|
||||
);
|
||||
return next();
|
||||
};
|
||||
|
||||
@@ -26,16 +28,19 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
||||
const match = authHeader.match(/^Bearer (.+)$/);
|
||||
if (!match) {
|
||||
throw new VisibleError(
|
||||
"auth.token",
|
||||
"Bearer token not found or improperly formatted",
|
||||
"authentication",
|
||||
ErrorCodes.Authentication.INVALID_TOKEN,
|
||||
"Invalid personal access token",
|
||||
);
|
||||
}
|
||||
const bearerToken = match[1];
|
||||
let result = await client.verify(subjects, bearerToken!);
|
||||
if (result.err) {
|
||||
throw new HTTPException(401, {
|
||||
message: "Unauthorized",
|
||||
});
|
||||
throw new VisibleError(
|
||||
"authentication",
|
||||
ErrorCodes.Authentication.INVALID_TOKEN,
|
||||
"Invalid bearer token",
|
||||
);
|
||||
}
|
||||
|
||||
if (result.subject.type === "user") {
|
||||
@@ -50,20 +55,20 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
||||
},
|
||||
},
|
||||
next
|
||||
// async () => {
|
||||
// const user = await User.fromEmail(email);
|
||||
// if (!user || user.length === 0) {
|
||||
// c.status(401);
|
||||
// return c.text("Unauthorized");
|
||||
// }
|
||||
// return withActor(
|
||||
// {
|
||||
// type: "member",
|
||||
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
|
||||
// },
|
||||
// next,
|
||||
// );
|
||||
// },
|
||||
// async () => {
|
||||
// const user = await User.fromEmail(email);
|
||||
// if (!user || user.length === 0) {
|
||||
// c.status(401);
|
||||
// return c.text("Unauthorized");
|
||||
// }
|
||||
// return withActor(
|
||||
// {
|
||||
// type: "member",
|
||||
// properties: { userID: user[0].id, workspaceID: user.workspaceID },
|
||||
// },
|
||||
// next,
|
||||
// );
|
||||
// },
|
||||
);
|
||||
}
|
||||
};
|
||||
246
packages/functions/src/api/common.ts
Normal file
246
packages/functions/src/api/common.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import {type Hook } from "./hook";
|
||||
import { z, ZodSchema } from "zod";
|
||||
import { ErrorCodes, ErrorResponse } from "@nestri/core/error";
|
||||
import type { MiddlewareHandler, ValidationTargets } from "hono";
|
||||
import { resolver, validator as zodValidator } from "hono-openapi/zod";
|
||||
|
||||
export function Result<T extends z.ZodTypeAny>(schema: T) {
|
||||
return resolver(
|
||||
z.object({
|
||||
data: schema,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom validator wrapper around hono-openapi/zod validator that formats errors
|
||||
* according to our standard API error format
|
||||
*/
|
||||
export const validator = <
|
||||
T extends ZodSchema,
|
||||
Target extends keyof ValidationTargets
|
||||
>(
|
||||
target: Target,
|
||||
schema: T
|
||||
): MiddlewareHandler<
|
||||
any,
|
||||
string,
|
||||
{
|
||||
in: {
|
||||
[K in Target]: z.input<T>;
|
||||
};
|
||||
out: {
|
||||
[K in Target]: z.output<T>;
|
||||
};
|
||||
}
|
||||
> => {
|
||||
// Create a custom error handler that formats errors according to our standards
|
||||
// const standardErrorHandler: Parameters<typeof zodValidator>[2] = (
|
||||
const standardErrorHandler: Hook<z.infer<T>, any, any, Target> = (
|
||||
result,
|
||||
c,
|
||||
) => {
|
||||
if (!result.success) {
|
||||
// Get the validation issues
|
||||
const issues = result.error.issues || result.error.errors || [];
|
||||
if (issues.length === 0) {
|
||||
// If there are no issues, return a generic error
|
||||
return c.json(
|
||||
{
|
||||
type: "validation",
|
||||
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
message: "Invalid request data",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the first error for the main response
|
||||
const firstIssue = issues[0]!;
|
||||
const fieldPath = firstIssue.path
|
||||
? Array.isArray(firstIssue.path)
|
||||
? firstIssue.path.join(".")
|
||||
: firstIssue.path
|
||||
: undefined;
|
||||
|
||||
// Map Zod error codes to our standard error codes
|
||||
let errorCode = ErrorCodes.Validation.INVALID_PARAMETER;
|
||||
if (
|
||||
firstIssue.code === "invalid_type" &&
|
||||
firstIssue.received === "undefined"
|
||||
) {
|
||||
errorCode = ErrorCodes.Validation.MISSING_REQUIRED_FIELD;
|
||||
} else if (
|
||||
["invalid_string", "invalid_date", "invalid_regex"].includes(
|
||||
firstIssue.code,
|
||||
)
|
||||
) {
|
||||
errorCode = ErrorCodes.Validation.INVALID_FORMAT;
|
||||
}
|
||||
|
||||
// Create our standardized error response
|
||||
const response = {
|
||||
type: "validation",
|
||||
code: errorCode,
|
||||
message: firstIssue.message,
|
||||
param: fieldPath,
|
||||
details: undefined as any,
|
||||
};
|
||||
|
||||
// Add details if we have multiple issues
|
||||
if (issues.length > 0) {
|
||||
response.details = {
|
||||
issues: issues.map((issue) => ({
|
||||
path: issue.path
|
||||
? Array.isArray(issue.path)
|
||||
? issue.path.join(".")
|
||||
: issue.path
|
||||
: undefined,
|
||||
code: issue.code,
|
||||
message: issue.message,
|
||||
// @ts-expect-error
|
||||
expected: issue.expected,
|
||||
// @ts-expect-error
|
||||
received: issue.received,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
console.log("Validation error in validator:", response);
|
||||
return c.json(response, 400);
|
||||
}
|
||||
};
|
||||
|
||||
// Use the original validator with our custom error handler
|
||||
return zodValidator(target, schema, standardErrorHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard error responses for OpenAPI documentation
|
||||
*/
|
||||
export const ErrorResponses = {
|
||||
400: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Validation error",
|
||||
example: {
|
||||
type: "validation",
|
||||
code: "invalid_parameter",
|
||||
message: "The request was invalid",
|
||||
param: "email",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Bad Request - The request could not be understood or was missing required parameters.",
|
||||
},
|
||||
401: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Authentication error",
|
||||
example: {
|
||||
type: "authentication",
|
||||
code: "unauthorized",
|
||||
message: "Authentication required",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Unauthorized - Authentication is required and has failed or has not been provided.",
|
||||
},
|
||||
403: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Permission error",
|
||||
example: {
|
||||
type: "forbidden",
|
||||
code: "permission_denied",
|
||||
message: "You do not have permission to access this resource",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Forbidden - You do not have permission to access this resource.",
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Not found error",
|
||||
example: {
|
||||
type: "not_found",
|
||||
code: "resource_not_found",
|
||||
message: "The requested resource could not be found",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Not Found - The requested resource does not exist.",
|
||||
},
|
||||
409: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Conflict Error",
|
||||
example: {
|
||||
type: "already_exists",
|
||||
code: "resource_already_exists",
|
||||
message: "The resource could not be created because it already exists",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Conflict - The resource could not be created because it already exists.",
|
||||
},
|
||||
429: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Rate limit error",
|
||||
example: {
|
||||
type: "rate_limit",
|
||||
code: "too_many_requests",
|
||||
message: "Rate limit exceeded",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description:
|
||||
"Too Many Requests - You have made too many requests in a short period of time.",
|
||||
},
|
||||
500: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
ErrorResponse.openapi({
|
||||
description: "Server error",
|
||||
example: {
|
||||
type: "internal",
|
||||
code: "internal_error",
|
||||
message: "Internal server error",
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Internal Server Error - Something went wrong on our end.",
|
||||
},
|
||||
};
|
||||
20
packages/functions/src/api/hook.ts
Normal file
20
packages/functions/src/api/hook.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ZodError, ZodSchema, z } from 'zod';
|
||||
import type { Env, ValidationTargets, Context, TypedResponse, Input, MiddlewareHandler } from 'hono';
|
||||
|
||||
type Hook<T, E extends Env, P extends string, Target extends keyof ValidationTargets = keyof ValidationTargets, O = {}> = (result: ({
|
||||
success: true;
|
||||
data: T;
|
||||
} | {
|
||||
success: false;
|
||||
error: ZodError;
|
||||
data: T;
|
||||
}) & {
|
||||
target: Target;
|
||||
}, c: Context<E, P>) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>>;
|
||||
type HasUndefined<T> = undefined extends T ? true : false;
|
||||
declare const zValidator: <T extends ZodSchema<any, z.ZodTypeDef, any>, Target extends keyof ValidationTargets, E extends Env, P extends string, In = z.input<T>, Out = z.output<T>, I extends Input = {
|
||||
in: HasUndefined<In> extends true ? { [K in Target]?: (In extends ValidationTargets[K] ? In : { [K2 in keyof In]?: ValidationTargets[K][K2] | undefined; }) | undefined; } : { [K_1 in Target]: In extends ValidationTargets[K_1] ? In : { [K2_1 in keyof In]: ValidationTargets[K_1][K2_1]; }; };
|
||||
out: { [K_2 in Target]: Out; };
|
||||
}, V extends I = I>(target: Target, schema: T, hook?: Hook<z.TypeOf<T>, E, P, Target, {}> | undefined) => MiddlewareHandler<E, P, V>;
|
||||
|
||||
export { type Hook, zValidator };
|
||||
@@ -1,16 +1,16 @@
|
||||
import "zod-openapi/extend";
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth";
|
||||
import { ZodError } from "zod";
|
||||
import { TeamApi } from "./team";
|
||||
import { logger } from "hono/logger";
|
||||
import { AccountApi } from "./account";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
import { VisibleError } from "@nestri/core/error";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { handle, streamHandle } from "hono/aws-lambda";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
|
||||
const app = new Hono();
|
||||
export const app = new Hono();
|
||||
app
|
||||
.use(logger(), async (c, next) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
@@ -20,57 +20,47 @@ app
|
||||
|
||||
const routes = app
|
||||
.get("/", (c) => c.text("Hello World!"))
|
||||
.route("/team", TeamApi.route)
|
||||
.route("/account", AccountApi.route)
|
||||
.onError((error, c) => {
|
||||
console.warn(error);
|
||||
if (error instanceof VisibleError) {
|
||||
return c.json(
|
||||
{
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
if (error instanceof ZodError) {
|
||||
const e = error.errors[0];
|
||||
if (e) {
|
||||
return c.json(
|
||||
{
|
||||
code: e?.code,
|
||||
message: e?.message,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
console.error("api error:", error);
|
||||
// @ts-expect-error
|
||||
return c.json(error.toResponse(), error.statusCode());
|
||||
}
|
||||
// Handle HTTP exceptions
|
||||
if (error instanceof HTTPException) {
|
||||
console.error("http error:", error);
|
||||
return c.json(
|
||||
{
|
||||
code: "request",
|
||||
type: "validation",
|
||||
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
message: "Invalid request",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
console.error("unhandled error:", error);
|
||||
return c.json(
|
||||
{
|
||||
code: "internal",
|
||||
type: "internal",
|
||||
code: ErrorCodes.Server.INTERNAL_ERROR,
|
||||
message: "Internal server error",
|
||||
},
|
||||
500,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
app.get(
|
||||
"/doc",
|
||||
openAPISpecs(routes, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Nestri API",
|
||||
description:
|
||||
"The Nestri API gives you the power to run your own customized cloud gaming platform.",
|
||||
version: "0.3.0",
|
||||
description: "The Nestri API gives you the power to run your own customized cloud gaming platform.",
|
||||
version: "0.0.1",
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
@@ -81,13 +71,13 @@ app.get(
|
||||
},
|
||||
TeamID: {
|
||||
type: "apiKey",
|
||||
description:"The team ID to use for this query",
|
||||
description: "The team ID to use for this query",
|
||||
in: "header",
|
||||
name: "x-nestri-team"
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ Bearer: [], TeamID:[] }],
|
||||
security: [{ Bearer: [], TeamID: [] }],
|
||||
servers: [
|
||||
{ description: "Production", url: "https://api.nestri.io" },
|
||||
],
|
||||
|
||||
95
packages/functions/src/api/team.ts
Normal file
95
packages/functions/src/api/team.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { z } from "zod";
|
||||
import { Hono } from "hono";
|
||||
import { notPublic } from "./auth";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { User } from "@nestri/core/user/index";
|
||||
import { Team } from "@nestri/core/team/index";
|
||||
import { Examples } from "@nestri/core/examples";
|
||||
import { Member } from "@nestri/core/member/index";
|
||||
import { assertActor, withActor } from "@nestri/core/actor";
|
||||
import { ErrorResponses, Result, validator } from "./common";
|
||||
|
||||
export module TeamApi {
|
||||
export const route = new Hono()
|
||||
.use(notPublic)
|
||||
.get("/",
|
||||
describeRoute({
|
||||
tags: ["Team"],
|
||||
summary: "List teams",
|
||||
description: "List the teams associated with the current user",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
Team.Info.array().openapi({
|
||||
description: "List of teams",
|
||||
example: [Examples.Team]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "List of teams"
|
||||
},
|
||||
}
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json({
|
||||
data: await User.teams()
|
||||
}, 200);
|
||||
},
|
||||
)
|
||||
.post("/",
|
||||
describeRoute({
|
||||
tags: ["Team"],
|
||||
summary: "Create a team",
|
||||
description: "Create a team for the current user",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.literal("ok")
|
||||
)
|
||||
}
|
||||
},
|
||||
description: "Team created succesfully"
|
||||
},
|
||||
400: ErrorResponses[400],
|
||||
409: ErrorResponses[409],
|
||||
429: ErrorResponses[429],
|
||||
500: ErrorResponses[500],
|
||||
}
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
Team.create.schema.omit({ id: true }).openapi({
|
||||
description: "Details of the team to create",
|
||||
//@ts-expect-error
|
||||
example: { ...Examples.Team, id: undefined }
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const actor = assertActor("user");
|
||||
|
||||
const teamID = await Team.create(body);
|
||||
|
||||
await withActor(
|
||||
{
|
||||
type: "system",
|
||||
properties: {
|
||||
teamID,
|
||||
},
|
||||
},
|
||||
() =>
|
||||
Member.create({
|
||||
first: true,
|
||||
email: actor.properties.email,
|
||||
}),
|
||||
);
|
||||
|
||||
return c.json({ data: "ok" })
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -136,12 +136,17 @@ const app = issuer({
|
||||
return ctx.subject("user", {
|
||||
userID,
|
||||
email
|
||||
}, {
|
||||
subject: email
|
||||
});
|
||||
|
||||
} else if (matching) {
|
||||
//Sign In
|
||||
return ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email
|
||||
}, {
|
||||
subject: email
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -175,12 +180,16 @@ const app = issuer({
|
||||
return ctx.subject("user", {
|
||||
userID,
|
||||
email: user.primary.email
|
||||
}, {
|
||||
subject: user.primary.email
|
||||
});
|
||||
} else {
|
||||
//Sign In
|
||||
return await ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email: user.primary.email
|
||||
}, {
|
||||
subject: user.primary.email
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
|
||||
export function Result<T extends z.ZodTypeAny>(schema: T) {
|
||||
return resolver(
|
||||
z.object({
|
||||
data: schema,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
8
packages/functions/src/migrator.ts
Normal file
8
packages/functions/src/migrator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { db } from "@nestri/core/drizzle/index";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
|
||||
export const handler = async (event: any) => {
|
||||
await migrate(db, {
|
||||
migrationsFolder: "./migrations",
|
||||
});
|
||||
};
|
||||
10
packages/functions/src/zero.ts
Normal file
10
packages/functions/src/zero.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
// import postgres from "postgres";
|
||||
import { db, sql } from "@nestri/core/drizzle/index";
|
||||
|
||||
export async function handler() {
|
||||
// const sql = postgres(process.env.ZERO_UPSTREAM_DB!);
|
||||
const perms = fs.readFileSync(".permissions.sql", "utf8");
|
||||
// await sql.unsafe(perms);
|
||||
await db.execute(sql.raw(perms))
|
||||
}
|
||||
Reference in New Issue
Block a user