mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-14 16:46:22 +08:00
c53457691d
#### PR Dependency Tree * **PR #14996** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Admin mutations to grant/revoke commercial entitlements. * New Doc comment-update permission. * Realtime user/workspace quota-state endpoints and live-update rooms. * **Bug Fixes** * More accurate readable-doc filtering and permission evaluation. * **Refactor** * Workspace feature management moved to entitlement-based model; permission and quota pipelines redesigned. * Admin workspace UI now edits flags only (feature toggles removed). * **Tests** * Extensive new and updated tests for permissions, entitlements, quota, projection, and backfills. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14996?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
224 lines
5.7 KiB
TypeScript
224 lines
5.7 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import { BaseModel } from './base';
|
|
|
|
export type WorkspaceRuntimeState = {
|
|
workspaceId: string;
|
|
known: boolean;
|
|
stale: boolean;
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
updatedAt: Date | null;
|
|
lastReconciledAt: Date | null;
|
|
staleAfter: Date | null;
|
|
};
|
|
|
|
type WorkspaceRuntimeStateRow = {
|
|
workspaceId: string;
|
|
known: boolean;
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
updatedAt: Date;
|
|
lastReconciledAt: Date | null;
|
|
staleAfter: Date | null;
|
|
};
|
|
|
|
type LegacyWorkspaceRuntimeStateRow = {
|
|
workspaceId: string;
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
updatedAt: Date;
|
|
staleAt: Date | null;
|
|
};
|
|
|
|
function isMissingRuntimeStateColumn(error: unknown) {
|
|
const meta = (error as { meta?: { code?: string } })?.meta;
|
|
return meta?.code === '42703';
|
|
}
|
|
|
|
@Injectable()
|
|
export class WorkspaceRuntimeStateModel extends BaseModel {
|
|
private hasCurrentColumns?: Promise<boolean>;
|
|
|
|
async get(workspaceId: string): Promise<WorkspaceRuntimeState> {
|
|
const rows = await this.loadRows(workspaceId);
|
|
const row = rows[0];
|
|
|
|
if (!row) {
|
|
return {
|
|
workspaceId,
|
|
known: false,
|
|
stale: true,
|
|
readonly: false,
|
|
readonlyReasons: [],
|
|
updatedAt: null,
|
|
lastReconciledAt: null,
|
|
staleAfter: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
workspaceId,
|
|
known: row.known,
|
|
stale:
|
|
!row.known || (row.staleAfter !== null && row.staleAfter <= new Date()),
|
|
readonly: row.readonly,
|
|
readonlyReasons: row.readonlyReasons,
|
|
updatedAt: row.updatedAt,
|
|
lastReconciledAt: row.lastReconciledAt,
|
|
staleAfter: row.staleAfter,
|
|
};
|
|
}
|
|
|
|
async upsert(
|
|
workspaceId: string,
|
|
state: {
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
known?: boolean;
|
|
lastReconciledAt?: Date | null;
|
|
staleAfter?: Date | null;
|
|
}
|
|
) {
|
|
if (await this.supportsCurrentRuntimeStateColumns()) {
|
|
await this.upsertCurrent(workspaceId, state);
|
|
} else {
|
|
await this.upsertLegacy(workspaceId, state);
|
|
}
|
|
}
|
|
|
|
private async loadRows(workspaceId: string) {
|
|
if (!(await this.supportsCurrentRuntimeStateColumns())) {
|
|
return await this.loadLegacyRows(workspaceId);
|
|
}
|
|
|
|
try {
|
|
return await this.db.$queryRaw<WorkspaceRuntimeStateRow[]>`
|
|
SELECT
|
|
workspace_id AS "workspaceId",
|
|
known,
|
|
readonly,
|
|
readonly_reasons AS "readonlyReasons",
|
|
updated_at AS "updatedAt",
|
|
last_reconciled_at AS "lastReconciledAt",
|
|
stale_after AS "staleAfter"
|
|
FROM workspace_runtime_states
|
|
WHERE workspace_id = ${workspaceId}
|
|
LIMIT 1
|
|
`;
|
|
} catch (error) {
|
|
if (!isMissingRuntimeStateColumn(error)) {
|
|
throw error;
|
|
}
|
|
return await this.loadLegacyRows(workspaceId);
|
|
}
|
|
}
|
|
|
|
private async supportsCurrentRuntimeStateColumns() {
|
|
this.hasCurrentColumns ??= this.db.$queryRaw<Array<{ exists: boolean }>>`
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'workspace_runtime_states'
|
|
AND column_name = 'known'
|
|
) AS "exists"
|
|
`.then(rows => rows[0]?.exists ?? false);
|
|
return await this.hasCurrentColumns;
|
|
}
|
|
|
|
private async loadLegacyRows(workspaceId: string) {
|
|
const rows = await this.db.$queryRaw<LegacyWorkspaceRuntimeStateRow[]>`
|
|
SELECT
|
|
workspace_id AS "workspaceId",
|
|
readonly,
|
|
readonly_reasons AS "readonlyReasons",
|
|
updated_at AS "updatedAt",
|
|
stale_at AS "staleAt"
|
|
FROM workspace_runtime_states
|
|
WHERE workspace_id = ${workspaceId}
|
|
LIMIT 1
|
|
`;
|
|
return rows.map(row => ({
|
|
workspaceId: row.workspaceId,
|
|
known: true,
|
|
readonly: row.readonly,
|
|
readonlyReasons: row.readonlyReasons,
|
|
updatedAt: row.updatedAt,
|
|
lastReconciledAt: row.updatedAt,
|
|
staleAfter: row.staleAt,
|
|
}));
|
|
}
|
|
|
|
private async upsertCurrent(
|
|
workspaceId: string,
|
|
state: {
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
known?: boolean;
|
|
lastReconciledAt?: Date | null;
|
|
staleAfter?: Date | null;
|
|
}
|
|
) {
|
|
await this.db.$executeRaw`
|
|
INSERT INTO workspace_runtime_states (
|
|
workspace_id,
|
|
known,
|
|
readonly,
|
|
readonly_reasons,
|
|
last_reconciled_at,
|
|
stale_after,
|
|
updated_at
|
|
)
|
|
VALUES (
|
|
${workspaceId},
|
|
${state.known ?? true},
|
|
${state.readonly},
|
|
${state.readonlyReasons},
|
|
${state.lastReconciledAt ?? new Date()},
|
|
${state.staleAfter ?? null},
|
|
now()
|
|
)
|
|
ON CONFLICT (workspace_id)
|
|
DO UPDATE SET
|
|
known = EXCLUDED.known,
|
|
readonly = EXCLUDED.readonly,
|
|
readonly_reasons = EXCLUDED.readonly_reasons,
|
|
last_reconciled_at = EXCLUDED.last_reconciled_at,
|
|
stale_after = EXCLUDED.stale_after,
|
|
updated_at = now()
|
|
`;
|
|
}
|
|
|
|
private async upsertLegacy(
|
|
workspaceId: string,
|
|
state: {
|
|
readonly: boolean;
|
|
readonlyReasons: string[];
|
|
staleAfter?: Date | null;
|
|
}
|
|
) {
|
|
await this.db.$executeRaw`
|
|
INSERT INTO workspace_runtime_states (
|
|
workspace_id,
|
|
readonly,
|
|
readonly_reasons,
|
|
stale_at,
|
|
updated_at
|
|
)
|
|
VALUES (
|
|
${workspaceId},
|
|
${state.readonly},
|
|
${state.readonlyReasons},
|
|
${state.staleAfter ?? null},
|
|
now()
|
|
)
|
|
ON CONFLICT (workspace_id)
|
|
DO UPDATE SET
|
|
readonly = EXCLUDED.readonly,
|
|
readonly_reasons = EXCLUDED.readonly_reasons,
|
|
stale_at = EXCLUDED.stale_at,
|
|
updated_at = now()
|
|
`;
|
|
}
|
|
}
|