feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,385 @@
import * as z from "zod";
import {
InvitationStatus,
MemberRole,
SocialProvider,
UserRole,
} from "@turbostarter/auth";
import { BillingStatus, PricingPlanType } from "@turbostarter/billing";
import { updateCustomerSchema } from "@turbostarter/db/schema";
import {
offsetPaginationSchema,
sortSchema,
} from "@turbostarter/shared/schema";
export const getUsersInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
role: z
.union([
z.enum(UserRole).transform((val) => [val]),
z.array(z.enum(UserRole)),
])
.optional(),
twoFactorEnabled: z
.union([
z.coerce.boolean().transform((val) => [val]),
z.array(z.coerce.boolean()),
])
.optional(),
banned: z
.union([
z.coerce.boolean().transform((val) => [val]),
z.array(z.coerce.boolean()),
])
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetUsersInput = z.infer<typeof getUsersInputSchema>;
export const getUsersResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
email: z.string(),
emailVerified: z.boolean(),
name: z.string(),
image: z.string().nullish(),
twoFactorEnabled: z.boolean().nullable(),
isAnonymous: z.boolean(),
banned: z.boolean().nullable(),
role: z.string().nullish(),
banReason: z.string().nullish(),
banExpires: z.coerce.date().nullish(),
}),
),
total: z.number(),
});
export type GetUsersResponse = z.infer<typeof getUsersResponseSchema>;
export const getUserAccountsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
providerId: z
.union([
z
.enum(["credential", ...Object.values(SocialProvider)])
.transform((val) => [val]),
z.array(z.enum(["credential", ...Object.values(SocialProvider)])),
])
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
updatedAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetUserAccountsInput = z.infer<typeof getUserAccountsInputSchema>;
export const getUserPlansInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
plan: z
.union([
z.enum(PricingPlanType).transform((val) => [val]),
z.array(z.enum(PricingPlanType)),
])
.nullable()
.optional(),
status: z
.union([
z.enum(BillingStatus).transform((val) => [val]),
z.array(z.enum(BillingStatus)),
])
.nullable()
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetUserPlansInput = z.infer<typeof getUserPlansInputSchema>;
export const getUserPlansResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
userId: z.string(),
customerId: z.string(),
plan: z.enum(PricingPlanType).nullable(),
status: z.enum(BillingStatus).nullable(),
credits: z.number(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
user: z.object({
name: z.string(),
}),
}),
),
total: z.number(),
});
export type GetUserPlansResponse = z.infer<typeof getUserPlansResponseSchema>;
export const getUserAccountsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
userId: z.string(),
providerId: z.string(),
accountId: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
}),
),
total: z.number(),
});
export type GetUserAccountsResponse = z.infer<
typeof getUserAccountsResponseSchema
>;
export const getUserMembershipsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
role: z
.union([
z.enum(MemberRole).transform((val) => [val]),
z.array(z.enum(MemberRole)),
])
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetUserMembershipsInput = z.infer<
typeof getUserMembershipsInputSchema
>;
export const getUserMembershipsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
organizationId: z.string(),
role: z.enum(MemberRole),
createdAt: z.coerce.date(),
userId: z.string(),
organization: z.object({
id: z.string(),
name: z.string(),
slug: z.string().nullish(),
logo: z.string().nullish(),
}),
user: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
}),
),
total: z.number(),
});
export type GetUserMembershipsResponse = z.infer<
typeof getUserMembershipsResponseSchema
>;
export const getUserInvitationsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
role: z
.union([
z.enum(MemberRole).transform((val) => [val]),
z.array(z.enum(MemberRole)),
])
.optional(),
status: z
.union([
z.enum(InvitationStatus).transform((val) => [val]),
z.array(z.enum(InvitationStatus)),
])
.optional(),
expiresAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetUserInvitationsInput = z.infer<
typeof getUserInvitationsInputSchema
>;
export const getUserInvitationsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
email: z.string(),
role: z.enum(MemberRole),
status: z.enum(InvitationStatus),
expiresAt: z.coerce.date(),
createdAt: z.coerce.date(),
inviterId: z.string(),
organizationId: z.string(),
organization: z.object({
id: z.string(),
name: z.string(),
logo: z.string().nullish(),
}),
}),
),
total: z.number(),
});
export type GetUserInvitationsResponse = z.infer<
typeof getUserInvitationsResponseSchema
>;
export const getOrganizationsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
members: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetOrganizationsInput = z.infer<typeof getOrganizationsInputSchema>;
export const getOrganizationsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string().nullish(),
logo: z.string().nullish(),
createdAt: z.coerce.date(),
members: z.number(),
}),
),
total: z.number(),
max: z.object({
members: z.number(),
}),
});
export type GetOrganizationsResponse = z.infer<
typeof getOrganizationsResponseSchema
>;
export const getOrganizationResponseSchema = z
.object({
id: z.string(),
name: z.string(),
slug: z.string().nullish(),
logo: z.string().nullish(),
createdAt: z.coerce.date(),
})
.nullable();
export type GetOrganizationResponse = z.infer<
typeof getOrganizationResponseSchema
>;
export const getCustomersInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
plan: z
.union([
z.enum(PricingPlanType).transform((val) => [val]),
z.array(z.enum(PricingPlanType)),
])
.optional(),
status: z
.union([
z.enum(BillingStatus).transform((val) => [val]),
z.array(z.enum(BillingStatus)),
])
.optional(),
createdAt: z.tuple([z.coerce.number(), z.coerce.number()]).optional(),
});
export type GetCustomersInput = z.infer<typeof getCustomersInputSchema>;
export const getCustomersResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
customerId: z.string(),
userId: z.string(),
plan: z.enum(PricingPlanType).nullable(),
status: z.enum(BillingStatus).nullable(),
credits: z.number(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
user: z.object({
name: z.string(),
image: z.string().nullish(),
}),
}),
),
total: z.number(),
});
export type GetCustomersResponse = z.infer<typeof getCustomersResponseSchema>;
export { updateCustomerSchema as updateCustomerInputSchema };
export type UpdateCustomerInput = z.infer<typeof updateCustomerSchema>;
// ============================================================================
// Credit Management Schemas
// ============================================================================
export const updateCreditsSchema = z.object({
action: z.enum(["set", "add", "deduct"]),
amount: z.number().int().positive(),
reason: z.string().max(500).optional(),
});
export type UpdateCreditsInput = z.infer<typeof updateCreditsSchema>;
export const getTransactionsSchema = z.object({
customerId: z.string(),
page: z.number().int().positive().default(1),
perPage: z.number().int().positive().max(100).default(20),
type: z
.enum([
"signup",
"purchase",
"usage",
"admin_grant",
"admin_deduct",
"refund",
"promo",
"referral",
"expiry",
])
.optional(),
});
export type GetTransactionsInput = z.infer<typeof getTransactionsSchema>;

View File

@@ -0,0 +1,2 @@
export * from "./admin";
export * from "./organization";

View File

@@ -0,0 +1,95 @@
import * as z from "zod";
import { InvitationStatus, MemberRole } from "@turbostarter/auth";
import {
offsetPaginationSchema,
sortSchema,
} from "@turbostarter/shared/schema";
export const getMembersInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
q: z.string().optional(),
role: z
.union([
z.enum(MemberRole).transform((val) => [val]),
z.array(z.enum(MemberRole)),
])
.optional(),
createdAt: z.tuple([z.number(), z.number()]).optional(),
});
export type GetMembersInput = z.infer<typeof getMembersInputSchema>;
export const getMembersResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
organizationId: z.string(),
role: z.enum(MemberRole),
createdAt: z.coerce.date(),
userId: z.string(),
user: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
image: z
.string()
.nullish()
.transform((val) => (val === null ? undefined : val)),
}),
}),
),
total: z.number(),
});
export type GetMembersResponse = z.infer<typeof getMembersResponseSchema>;
export const getInvitationsInputSchema = offsetPaginationSchema.extend({
sort: z
.string()
.transform((val) =>
z.array(sortSchema).parse(JSON.parse(decodeURIComponent(val))),
)
.optional(),
email: z.string().optional(),
role: z
.union([
z.enum(MemberRole).transform((val) => [val]),
z.array(z.enum(MemberRole)),
])
.optional(),
status: z
.union([
z.enum(InvitationStatus).transform((val) => [val]),
z.array(z.enum(InvitationStatus)),
])
.optional(),
expiresAt: z.tuple([z.number(), z.number()]).optional(),
});
export type GetInvitationsInput = z.infer<typeof getInvitationsInputSchema>;
export const getInvitationsResponseSchema = z.object({
data: z.array(
z.object({
id: z.string(),
organizationId: z.string(),
email: z.string(),
role: z.enum(MemberRole),
expiresAt: z.coerce.date(),
createdAt: z.coerce.date(),
inviterId: z.string(),
status: z.enum(InvitationStatus),
}),
),
total: z.number(),
});
export type GetInvitationsResponse = z.infer<
typeof getInvitationsResponseSchema
>;