Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): team quota #8955

Open
wants to merge 32 commits into
base: 61/team-workspace-subscription
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
70b21ee
feat: init team quota
darkskygit Nov 28, 2024
af60097
feat: add override for quota
darkskygit Nov 28, 2024
9f647a7
feat: team feature & test case
darkskygit Nov 28, 2024
afb3bc7
feat: improve override
darkskygit Nov 28, 2024
d58ea10
chore: fix lint
darkskygit Nov 28, 2024
8d7bc81
feat: member status
darkskygit Nov 29, 2024
48f9884
chore: update default values
darkskygit Nov 29, 2024
3341643
feat: new team invite logic
darkskygit Nov 29, 2024
c0ae0cc
fix: lint
darkskygit Nov 29, 2024
b5f8589
feat: add index
darkskygit Nov 29, 2024
7fc4f70
feat: workspace permission
darkskygit Nov 29, 2024
7177e91
feat: grant permission for exists member
darkskygit Nov 29, 2024
091bf5e
fix: merge error
darkskygit Dec 2, 2024
2fdbd05
feat: adapt team subscription event
darkskygit Dec 2, 2024
b76ec5e
feat: improve typing & naming
darkskygit Dec 2, 2024
dc24a80
feat: update team workspace config
darkskygit Dec 3, 2024
b8a267d
feat: team config query
darkskygit Dec 3, 2024
8c1cdab
feat: decline invite
darkskygit Dec 3, 2024
03e0f9c
feat: split resolver
darkskygit Dec 3, 2024
042e449
chore: mark todos
darkskygit Dec 3, 2024
f4f65d6
feat: add more gql
darkskygit Dec 3, 2024
abc8ec6
fix: seat calc
darkskygit Dec 3, 2024
188c2d5
fix: feature check
darkskygit Dec 3, 2024
873bb51
chore: fix missing history override
darkskygit Dec 3, 2024
e63990e
feat: invite batch
darkskygit Dec 3, 2024
fd2a25e
chore: remove declined status
darkskygit Dec 4, 2024
f92b071
fix: event typo
darkskygit Dec 4, 2024
feba3f9
feat: add workspace level feature flags
darkskygit Dec 4, 2024
dc0d42a
feat: move team workspace to quota
darkskygit Dec 4, 2024
e6f892c
chore: cleanup codes
darkskygit Dec 4, 2024
7f5cb3a
feat: update gql
darkskygit Dec 4, 2024
0aa39e9
feat: handle member updated event
darkskygit Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- CreateEnum
CREATE TYPE "WorkspaceMemberStatus" AS ENUM ('Pending', 'NeedMoreSeat', 'UnderReview', 'Accepted');

-- AlterTable
ALTER TABLE "workspace_features" ADD COLUMN "configs" JSON NOT NULL DEFAULT '{}';
darkskygit marked this conversation as resolved.
Show resolved Hide resolved

-- AlterTable
ALTER TABLE "workspace_user_permissions" ADD COLUMN "status" "WorkspaceMemberStatus" NOT NULL DEFAULT 'Pending',
ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

-- CreateIndex
CREATE INDEX "workspace_features_workspace_id_idx" ON "workspace_features"("workspace_id");
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "enable_ai" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "enable_share" BOOLEAN NOT NULL DEFAULT true;
37 changes: 27 additions & 10 deletions packages/backend/server/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ model VerificationToken {
model Workspace {
id String @id @default(uuid()) @db.VarChar
public Boolean
enableUrlPreview Boolean @default(false) @map("enable_url_preview")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
// workspace level feature flags
enableAi Boolean @default(true) @map("enable_ai")
enableShare Boolean @default(true) @map("enable_share")
enableUrlPreview Boolean @default(false) @map("enable_url_preview")

pages WorkspacePage[]
permissions WorkspaceUserPermission[]
Expand Down Expand Up @@ -126,21 +129,32 @@ model WorkspacePage {
@@map("workspace_pages")
}

enum WorkspaceMemberStatus {
Pending // 1. old state accepted = false
NeedMoreSeat // 2. for team: workspace owner need to buy more seat
UnderReview // 3. for team: member is under review
Accepted // 4. old state accepted = true
}

model WorkspaceUserPermission {
id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
userId String @map("user_id") @db.VarChar
id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
userId String @map("user_id") @db.VarChar
// Read/Write
type Int @db.SmallInt
/// Whether the permission invitation is accepted by the user
accepted Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
type Int @db.SmallInt
/// @deprecated Whether the permission invitation is accepted by the user
accepted Boolean @default(false)
/// Whether the invite status of the workspace member
status WorkspaceMemberStatus @default(Pending)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
/// When the invite status changed
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

@@unique([workspaceId, userId])
// optimize for quering user's workspace permissions
// optimize for querying user's workspace permissions
@@index(userId)
@@map("workspace_user_permissions")
}
Expand Down Expand Up @@ -200,6 +214,8 @@ model WorkspaceFeature {
workspaceId String @map("workspace_id") @db.VarChar
featureId Int @map("feature_id") @db.Integer

// override quota's configs
configs Json @default("{}") @db.Json
// we will record the reason why the feature is enabled/disabled
// for example:
// - copilet_v1: "owner buy the copilet feature package"
Expand All @@ -216,6 +232,7 @@ model WorkspaceFeature {
feature Feature @relation(fields: [featureId], references: [id], onDelete: Cascade)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

@@index([workspaceId])
@@map("workspace_features")
}

Expand All @@ -225,7 +242,7 @@ model Feature {
version Int @default(0) @db.Integer
// 0: feature, 1: quota
type Int @db.Integer
// configs, define by feature conntroller
// configs, define by feature controller
configs Json @db.Json
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/core/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import { FeatureService } from './service';
})
export class FeatureModule {}

export type { FeatureConfigType } from './feature';
export {
type CommonFeature,
commonFeatureSchema,
type FeatureConfig,
FeatureKind,
Features,
FeatureType,
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/server/src/core/features/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class FeatureManagementService {
}

async listEarlyAccess(type: EarlyAccessType = EarlyAccessType.App) {
return this.feature.listFeatureUsers(
return this.feature.listUsersByFeature(
type === EarlyAccessType.App
? FeatureType.EarlyAccess
: FeatureType.AIEarlyAccess
Expand Down Expand Up @@ -132,7 +132,7 @@ export class FeatureManagementService {

// ======== User Feature ========
async getActivatedUserFeatures(userId: string): Promise<FeatureType[]> {
const features = await this.feature.getActivatedUserFeatures(userId);
const features = await this.feature.getUserActivatedFeatures(userId);
return features.map(f => f.feature.name);
}

Expand Down Expand Up @@ -165,7 +165,7 @@ export class FeatureManagementService {
}

async listFeatureWorkspaces(feature: FeatureType) {
return this.feature.listFeatureWorkspaces(feature);
return this.feature.listWorkspacesByFeature(feature);
}

@OnEvent('user.admin.created')
Expand Down
17 changes: 7 additions & 10 deletions packages/backend/server/src/core/features/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ export class FeatureService {

async getFeature<F extends FeatureType>(feature: F) {
const data = await this.prisma.feature.findFirst({
where: {
feature,
type: FeatureKind.Feature,
},
where: { feature, type: FeatureKind.Feature },
select: { id: true },
orderBy: {
version: 'desc',
},
orderBy: { version: 'desc' },
});

if (data) {
Expand Down Expand Up @@ -146,7 +141,7 @@ export class FeatureService {
return configs.filter(feature => !!feature.feature);
}

async getActivatedUserFeatures(userId: string) {
async getUserActivatedFeatures(userId: string) {
const features = await this.prisma.userFeature.findMany({
where: {
userId,
Expand All @@ -173,7 +168,7 @@ export class FeatureService {
return configs.filter(feature => !!feature.feature);
}

async listFeatureUsers(feature: FeatureType) {
async listUsersByFeature(feature: FeatureType) {
return this.prisma.userFeature
.findMany({
where: {
Expand Down Expand Up @@ -318,7 +313,9 @@ export class FeatureService {
return configs.filter(feature => !!feature.feature);
}

async listFeatureWorkspaces(feature: FeatureType): Promise<WorkspaceType[]> {
async listWorkspacesByFeature(
feature: FeatureType
): Promise<WorkspaceType[]> {
return this.prisma.workspaceFeature
.findMany({
where: {
Expand Down
24 changes: 14 additions & 10 deletions packages/backend/server/src/core/features/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,24 @@ export const Features: Feature[] = [

/// ======== schema infer ========

export const FeatureConfigSchema = z.discriminatedUnion('feature', [
featureCopilot,
featureEarlyAccess,
featureAIEarlyAccess,
featureUnlimitedWorkspace,
featureUnlimitedCopilot,
featureAdministrator,
]);

export const FeatureSchema = commonFeatureSchema
.extend({
type: z.literal(FeatureKind.Feature),
})
.and(
z.discriminatedUnion('feature', [
featureCopilot,
featureEarlyAccess,
featureAIEarlyAccess,
featureUnlimitedWorkspace,
featureUnlimitedCopilot,
featureAdministrator,
])
);
.and(FeatureConfigSchema);

export type FeatureConfig<F extends FeatureType> = (z.infer<
typeof FeatureConfigSchema
> & { feature: F })['configs'];

export type Feature = z.infer<typeof FeatureSchema>;

Expand Down
Loading
Loading