Openstatus www.openstatus.dev

refactor: workspace limits (#1094)

authored by

Maximilian Kaske and committed by
GitHub
0da65430 4e4e0acb

+55 -123
+2 -6
apps/web/src/config/pricing-table.tsx
··· 1 - import type { 2 - LimitsV1, 3 - LimitsV2, 4 - LimitsV3, 5 - } from "@openstatus/db/src/schema/plan/schema"; 1 + import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 6 2 import Link from "next/link"; 7 3 import type React from "react"; 8 4 ··· 30 26 { 31 27 label: string; 32 28 features: { 33 - value: keyof LimitsV1 | keyof LimitsV2 | keyof LimitsV3; 29 + value: keyof Limits; 34 30 label: string; 35 31 description?: React.ReactNode; // tooltip informations 36 32 badge?: string;
+42 -83
packages/db/src/schema/plan/schema.ts
··· 1 1 import { z } from "zod"; 2 2 import { monitorFlyRegionSchema, monitorPeriodicitySchema } from "../constants"; 3 3 4 - // This is not a database table but just a schema for the limits of the plan 4 + // REMINDER: this is not a database table but just a schema for the limits of the plan 5 + // default values are set to the free plan limits 5 6 6 - export const limitsV1 = z.object({ 7 + export const limitsSchema = z.object({ 7 8 version: z.undefined(), 8 - monitors: z.number(), 9 - "synthetic-checks": z.number(), 10 - periodicity: monitorPeriodicitySchema.array(), 11 - "multi-region": z.boolean(), 12 - "max-regions": z.number(), 13 - "data-retention": z.enum(["14 days", "3 months", "12 months", "24 months"]), 14 - // status pages 15 - "status-pages": z.number(), 16 - maintenance: z.boolean(), 17 - "status-subscribers": z.boolean(), 18 - "custom-domain": z.boolean(), 19 - "password-protection": z.boolean(), 20 - "white-label": z.boolean(), 21 - // alerts 22 - notifications: z.boolean(), 23 - pagerduty: z.boolean(), 24 - sms: z.boolean(), 25 - "notification-channels": z.number(), 26 - // collaboration 27 - members: z.literal("Unlimited").or(z.number()), 28 - "audit-log": z.boolean(), 29 - regions: monitorFlyRegionSchema.array(), 9 + /** 10 + * Monitor limits 11 + */ 12 + monitors: z.number().default(1), 13 + "synthetic-checks": z.number().default(30), 14 + periodicity: monitorPeriodicitySchema.array().default(["10m", "30m", "1h"]), 15 + "multi-region": z.boolean().default(true), 16 + "max-regions": z.number().default(6), 17 + "data-retention": z 18 + .enum(["14 days", "3 months", "12 months", "24 months"]) 19 + .default("14 days"), 20 + regions: monitorFlyRegionSchema 21 + .array() 22 + .default(["ams", "gru", "iad", "jnb", "hkg", "syd"]), 23 + "private-locations": z.boolean().default(false), 24 + screenshots: z.boolean().default(false), 25 + /** 26 + * Status page limits 27 + */ 28 + "status-pages": z.number().default(1), 29 + maintenance: z.boolean().default(true), 30 + "monitor-values-visibility": z.boolean().default(true), 31 + "status-subscribers": z.boolean().default(false), 32 + "custom-domain": z.boolean().default(false), 33 + "password-protection": z.boolean().default(false), 34 + "white-label": z.boolean().default(false), 35 + /** 36 + * Notification limits 37 + */ 38 + notifications: z.boolean().default(true), 39 + pagerduty: z.boolean().default(false), 40 + sms: z.boolean().default(false), 41 + "notification-channels": z.number().default(1), 42 + /** 43 + * Collaboration limits 44 + */ 45 + members: z.literal("Unlimited").or(z.number()).default(1), 46 + "audit-log": z.boolean().default(false), 30 47 }); 31 48 32 - export type LimitsV1 = z.infer<typeof limitsV1>; 33 - export const limitsV2 = limitsV1.extend({ 34 - version: z.literal("v2"), 35 - "private-locations": z.boolean(), 36 - "monitor-values-visibility": z.boolean(), 37 - }); 38 - 39 - export const limitsV3 = limitsV2.extend({ 40 - version: z.literal("v3"), 41 - screenshots: z.boolean(), 42 - }); 43 - 44 - export type LimitsV2 = z.infer<typeof limitsV2>; 45 - export type LimitsV3 = z.infer<typeof limitsV3>; 46 - 47 - const unknownLimit = z.discriminatedUnion("version", [ 48 - limitsV1, 49 - limitsV2, 50 - limitsV3, 51 - ]); 52 - 53 - export function migrateFromV1ToV2({ data }: { data: LimitsV1 }) { 54 - return { 55 - version: "v2", 56 - ...data, 57 - "private-locations": true, 58 - "monitor-values-visibility": true, 59 - }; 60 - } 61 - 62 - export function migrateFromV2ToV3({ data }: { data: LimitsV2 }) { 63 - return { 64 - ...data, 65 - version: "v3", 66 - screenshots: true, 67 - }; 68 - } 69 - 70 - export function migrateFromV1ToV3({ data }: { data: LimitsV1 }) { 71 - return { 72 - ...data, 73 - version: "v3", 74 - screenshots: true, 75 - "private-locations": true, 76 - "monitor-values-visibility": true, 77 - }; 78 - } 79 - 80 - export const limitSchema = unknownLimit.transform((val) => { 81 - if (!val.version) { 82 - return migrateFromV1ToV3({ data: val }); 83 - } 84 - if (val.version === "v2") { 85 - return migrateFromV2ToV3({ data: val }); 86 - } 87 - return val; 88 - }); 89 - 90 - export type Limits = z.infer<typeof unknownLimit>; 49 + export type Limits = z.infer<typeof limitsSchema>;
+11 -34
packages/db/src/schema/workspaces/validation.ts
··· 2 2 import { z } from "zod"; 3 3 4 4 import { allPlans } from "../plan/config"; 5 - import { limitsV1 } from "../plan/schema"; 5 + import { limitsSchema } from "../plan/schema"; 6 6 import { workspacePlans, workspaceRole } from "./constants"; 7 7 import { workspace } from "./workspace"; 8 8 ··· 11 11 12 12 /** 13 13 * Workspace schema with limits and plan 14 - * If not available in the db, the limits will be taken from the workspace plan 15 14 */ 16 - const selectWorkspaceSchemaDevelopment = createSelectSchema(workspace) 15 + export const selectWorkspaceSchema = createSelectSchema(workspace) 17 16 .extend({ 18 17 limits: z.string().transform((val) => { 19 18 const parsed = JSON.parse(val); 20 - const result = limitsV1.partial().safeParse(parsed); 19 + const result = limitsSchema.partial().safeParse(parsed); 21 20 if (result.error) return {}; 22 21 return result.data; 23 22 }), ··· 30 29 .transform((val) => { 31 30 return { 32 31 ...val, 33 - limits: limitsV1.parse({ ...allPlans[val.plan].limits, ...val.limits }), 32 + limits: limitsSchema.parse({ 33 + ...allPlans[val.plan].limits, 34 + /** 35 + * override the default plan limits 36 + * allows us to set custom limits for a workspace 37 + */ 38 + ...val.limits, 39 + }), 34 40 }; 35 41 }); 36 - 37 - /** 38 - * Workspace schema with limits and plan 39 - * The limits for paid users have to be defined within the db otherwise, fallbacks to free plan limits 40 - */ 41 - const selectWorkspaceSchemaProduction = createSelectSchema(workspace).extend({ 42 - limits: z.string().transform((val) => { 43 - const parsed = JSON.parse(val); 44 - const result = limitsV1.safeParse(parsed); 45 - if (result.error) { 46 - // Fallback to default limits 47 - return limitsV1.parse({ 48 - ...allPlans.free.limits, 49 - }); 50 - } 51 - 52 - return result.data; 53 - }), 54 - plan: z 55 - .enum(workspacePlans) 56 - .nullable() 57 - .default("free") 58 - .transform((val) => val ?? "free"), 59 - }); 60 - 61 - export const selectWorkspaceSchema = 62 - process.env.NODE_ENV === "development" 63 - ? selectWorkspaceSchemaDevelopment 64 - : selectWorkspaceSchemaProduction; 65 42 66 43 export const insertWorkspaceSchema = createSelectSchema(workspace); 67 44