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:
385
packages/api/src/schema/admin.ts
Normal file
385
packages/api/src/schema/admin.ts
Normal 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>;
|
||||
2
packages/api/src/schema/index.ts
Normal file
2
packages/api/src/schema/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./admin";
|
||||
export * from "./organization";
|
||||
95
packages/api/src/schema/organization.ts
Normal file
95
packages/api/src/schema/organization.ts
Normal 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
|
||||
>;
|
||||
Reference in New Issue
Block a user