mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
feat(server): entitlement based model (#14996)
#### 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 -->
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
import { Injectable, Optional } from '@nestjs/common';
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
import { metrics } from '../base';
|
||||
import { BaseModel } from './base';
|
||||
|
||||
type CountRow = { count: bigint };
|
||||
|
||||
type ProjectionIssueRow = {
|
||||
category: string;
|
||||
count: bigint;
|
||||
};
|
||||
|
||||
type ProjectionBackfillDb = {
|
||||
$transaction: (
|
||||
callback: (
|
||||
tx: Pick<Prisma.TransactionClient, '$executeRaw'>
|
||||
) => Promise<void>
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export type PermissionProjectionCheckReport = {
|
||||
oldWorkspacePolicyMismatch: number;
|
||||
oldAcceptedMemberMismatch: number;
|
||||
extraProjectedMember: number;
|
||||
oldInvitationMismatch: number;
|
||||
extraProjectedInvitation: number;
|
||||
oldDocGrantMismatch: number;
|
||||
extraProjectedDocGrant: number;
|
||||
oldDocPolicyMismatch: number;
|
||||
extraProjectedDocPolicy: number;
|
||||
runtimeStateMissing: number;
|
||||
runtimeStateMismatch: number;
|
||||
ownerConflict: number;
|
||||
oldNewDecisionMismatch: number;
|
||||
invalidLegacyRows: Record<string, number>;
|
||||
};
|
||||
|
||||
export const PERMISSION_PROJECTION_TRIGGER_ERROR_CATEGORIES = [
|
||||
'owner_conflict',
|
||||
'invalid_legacy_role',
|
||||
'foreign_key_missing',
|
||||
'projection_recursion_guard_missing',
|
||||
'unknown',
|
||||
] as const;
|
||||
|
||||
export function permissionProjectionTriggerErrorCategory(error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error ?? 'unknown');
|
||||
|
||||
const match = message.match(/permission_projection_error:([^:]+):/);
|
||||
const category = match?.[1];
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PERMISSION_PROJECTION_TRIGGER_ERROR_CATEGORIES.includes(
|
||||
category as (typeof PERMISSION_PROJECTION_TRIGGER_ERROR_CATEGORIES)[number]
|
||||
)
|
||||
? category
|
||||
: 'unknown';
|
||||
}
|
||||
|
||||
async function count(first: Promise<CountRow[]>) {
|
||||
const rows = await first;
|
||||
return Number(rows[0]?.count ?? 0);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PermissionProjectionModel extends BaseModel {
|
||||
constructor(@Optional() private readonly prisma?: PrismaClient) {
|
||||
super();
|
||||
}
|
||||
|
||||
async backfillLegacyProjection() {
|
||||
const db = (this.prisma ?? this.db) as unknown as ProjectionBackfillDb;
|
||||
|
||||
await db.$transaction(async tx => {
|
||||
await tx.$executeRaw`
|
||||
DELETE FROM workspace_members projected
|
||||
WHERE projected.legacy_permission_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspace_user_permissions old
|
||||
WHERE old.id = projected.legacy_permission_id
|
||||
AND old.status = 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_legacy_workspace_role(old.type) IS NOT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
DELETE FROM workspace_invitations projected
|
||||
WHERE projected.legacy_permission_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspace_user_permissions old
|
||||
WHERE old.id = projected.legacy_permission_id
|
||||
AND old.status <> 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_workspace_invitation_state(old.status) IS NOT NULL
|
||||
AND affine_permission_legacy_workspace_role(old.type) IS NOT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
DELETE FROM doc_grants projected
|
||||
WHERE projected.principal_type = 'user'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspace_page_user_permissions old
|
||||
WHERE old.workspace_id = projected.workspace_id
|
||||
AND old.page_id = projected.doc_id
|
||||
AND old.user_id = projected.principal_id
|
||||
AND affine_permission_legacy_doc_role(old.type) IS NOT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
DELETE FROM doc_access_policies projected
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspace_pages old
|
||||
WHERE old.workspace_id = projected.workspace_id
|
||||
AND old.page_id = projected.doc_id
|
||||
AND affine_permission_legacy_default_doc_role(old."defaultRole") IS NOT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
DELETE FROM workspace_access_policies projected
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspaces old
|
||||
WHERE old.id = projected.workspace_id
|
||||
)
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO workspace_access_policies (
|
||||
workspace_id,
|
||||
visibility,
|
||||
sharing_enabled,
|
||||
url_preview_enabled,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
CASE WHEN public THEN 'public' ELSE 'private' END,
|
||||
enable_sharing,
|
||||
enable_url_preview,
|
||||
now()
|
||||
FROM workspaces
|
||||
ON CONFLICT (workspace_id)
|
||||
DO UPDATE SET
|
||||
visibility = EXCLUDED.visibility,
|
||||
sharing_enabled = EXCLUDED.sharing_enabled,
|
||||
url_preview_enabled = EXCLUDED.url_preview_enabled,
|
||||
updated_at = now()
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO workspace_members (
|
||||
workspace_id,
|
||||
user_id,
|
||||
role,
|
||||
state,
|
||||
source,
|
||||
legacy_permission_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
workspace_id,
|
||||
user_id,
|
||||
affine_permission_legacy_workspace_role(type),
|
||||
'active',
|
||||
CASE source
|
||||
WHEN 'Email'::"WorkspaceMemberSource" THEN 'email'
|
||||
WHEN 'Link'::"WorkspaceMemberSource" THEN 'link'
|
||||
ELSE 'legacy'
|
||||
END,
|
||||
id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM workspace_user_permissions
|
||||
WHERE status = 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_legacy_workspace_role(type) IS NOT NULL
|
||||
ON CONFLICT ("legacy_permission_id") WHERE "legacy_permission_id" IS NOT NULL
|
||||
DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
role = EXCLUDED.role,
|
||||
state = EXCLUDED.state,
|
||||
source = EXCLUDED.source,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO workspace_invitations (
|
||||
workspace_id,
|
||||
invitee_user_id,
|
||||
inviter_user_id,
|
||||
requested_role,
|
||||
status,
|
||||
kind,
|
||||
legacy_permission_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
workspace_id,
|
||||
user_id,
|
||||
inviter_id,
|
||||
CASE WHEN affine_permission_legacy_workspace_role(type) = 'admin' THEN 'admin' ELSE 'member' END,
|
||||
affine_permission_workspace_invitation_state(status),
|
||||
CASE source
|
||||
WHEN 'Link'::"WorkspaceMemberSource" THEN 'link'
|
||||
ELSE 'email'
|
||||
END,
|
||||
id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM workspace_user_permissions
|
||||
WHERE status <> 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_workspace_invitation_state(status) IS NOT NULL
|
||||
AND affine_permission_legacy_workspace_role(type) IS NOT NULL
|
||||
ON CONFLICT ("legacy_permission_id") WHERE "legacy_permission_id" IS NOT NULL
|
||||
DO UPDATE SET
|
||||
invitee_user_id = EXCLUDED.invitee_user_id,
|
||||
inviter_user_id = EXCLUDED.inviter_user_id,
|
||||
requested_role = EXCLUDED.requested_role,
|
||||
status = EXCLUDED.status,
|
||||
kind = EXCLUDED.kind,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO doc_access_policies (
|
||||
workspace_id,
|
||||
doc_id,
|
||||
visibility,
|
||||
public_role,
|
||||
member_default_role,
|
||||
published_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
workspace_id,
|
||||
page_id,
|
||||
CASE WHEN public THEN 'public' ELSE 'private' END,
|
||||
CASE WHEN public THEN 'external' ELSE NULL END,
|
||||
affine_permission_legacy_default_doc_role("defaultRole"),
|
||||
published_at,
|
||||
now()
|
||||
FROM workspace_pages
|
||||
WHERE affine_permission_legacy_default_doc_role("defaultRole") IS NOT NULL
|
||||
ON CONFLICT (workspace_id, doc_id)
|
||||
DO UPDATE SET
|
||||
visibility = EXCLUDED.visibility,
|
||||
public_role = EXCLUDED.public_role,
|
||||
member_default_role = EXCLUDED.member_default_role,
|
||||
published_at = EXCLUDED.published_at,
|
||||
updated_at = now()
|
||||
`;
|
||||
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO doc_grants (
|
||||
workspace_id,
|
||||
doc_id,
|
||||
principal_type,
|
||||
principal_id,
|
||||
role,
|
||||
legacy_workspace_id,
|
||||
legacy_doc_id,
|
||||
legacy_user_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
workspace_id,
|
||||
page_id,
|
||||
'user',
|
||||
user_id,
|
||||
affine_permission_legacy_doc_role(type),
|
||||
workspace_id,
|
||||
page_id,
|
||||
user_id,
|
||||
created_at,
|
||||
now()
|
||||
FROM workspace_page_user_permissions
|
||||
WHERE affine_permission_legacy_doc_role(type) IS NOT NULL
|
||||
ON CONFLICT (workspace_id, doc_id, principal_type, principal_id)
|
||||
DO UPDATE SET
|
||||
role = EXCLUDED.role,
|
||||
updated_at = now()
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
recordTriggerErrorMetric(error: unknown) {
|
||||
const category = permissionProjectionTriggerErrorCategory(error);
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
metrics.permission
|
||||
.counter('projection_trigger_errors', {
|
||||
description: 'Permission projection trigger error count',
|
||||
})
|
||||
.add(1, { category });
|
||||
return category;
|
||||
}
|
||||
|
||||
async checkLegacyProjection(): Promise<PermissionProjectionCheckReport> {
|
||||
const [
|
||||
oldWorkspacePolicyMismatch,
|
||||
oldAcceptedMemberMismatch,
|
||||
extraProjectedMember,
|
||||
oldInvitationMismatch,
|
||||
extraProjectedInvitation,
|
||||
oldDocGrantMismatch,
|
||||
extraProjectedDocGrant,
|
||||
oldDocPolicyMismatch,
|
||||
extraProjectedDocPolicy,
|
||||
ownerConflict,
|
||||
invalidLegacyRows,
|
||||
] = await Promise.all([
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspaces old
|
||||
LEFT JOIN workspace_access_policies projected
|
||||
ON projected.workspace_id = old.id
|
||||
WHERE projected.workspace_id IS NULL
|
||||
OR projected.visibility <> CASE WHEN old.public THEN 'public' ELSE 'private' END
|
||||
OR projected.sharing_enabled <> old.enable_sharing
|
||||
OR projected.url_preview_enabled <> old.enable_url_preview
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_user_permissions old
|
||||
LEFT JOIN workspace_members projected
|
||||
ON projected.legacy_permission_id = old.id
|
||||
OR (
|
||||
projected.legacy_permission_id IS NULL
|
||||
AND projected.workspace_id = old.workspace_id
|
||||
AND projected.user_id = old.user_id
|
||||
AND projected.state = 'active'
|
||||
)
|
||||
WHERE old.status = 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_legacy_workspace_role(old.type) IS NOT NULL
|
||||
AND (
|
||||
projected.id IS NULL OR
|
||||
projected.workspace_id <> old.workspace_id OR
|
||||
projected.user_id <> old.user_id OR
|
||||
projected.role <> affine_permission_legacy_workspace_role(old.type) OR
|
||||
projected.state <> 'active'
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_members projected
|
||||
LEFT JOIN workspace_user_permissions old
|
||||
ON old.id = projected.legacy_permission_id
|
||||
OR (
|
||||
projected.legacy_permission_id IS NULL
|
||||
AND old.workspace_id = projected.workspace_id
|
||||
AND old.user_id = projected.user_id
|
||||
AND old.status = 'Accepted'::"WorkspaceMemberStatus"
|
||||
)
|
||||
WHERE
|
||||
projected.state = 'active'
|
||||
AND (
|
||||
old.id IS NULL OR
|
||||
old.status <> 'Accepted'::"WorkspaceMemberStatus" OR
|
||||
affine_permission_legacy_workspace_role(old.type) IS NULL
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_user_permissions old
|
||||
LEFT JOIN workspace_invitations projected
|
||||
ON projected.legacy_permission_id = old.id
|
||||
OR (
|
||||
projected.legacy_permission_id IS NULL
|
||||
AND projected.workspace_id = old.workspace_id
|
||||
AND projected.invitee_user_id = old.user_id
|
||||
)
|
||||
WHERE old.status <> 'Accepted'::"WorkspaceMemberStatus"
|
||||
AND affine_permission_workspace_invitation_state(old.status) IS NOT NULL
|
||||
AND affine_permission_legacy_workspace_role(old.type) IS NOT NULL
|
||||
AND (
|
||||
projected.id IS NULL OR
|
||||
projected.workspace_id <> old.workspace_id OR
|
||||
projected.invitee_user_id <> old.user_id OR
|
||||
projected.requested_role <> CASE WHEN affine_permission_legacy_workspace_role(old.type) = 'admin' THEN 'admin' ELSE 'member' END OR
|
||||
projected.status <> affine_permission_workspace_invitation_state(old.status)
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_invitations projected
|
||||
LEFT JOIN workspace_user_permissions old
|
||||
ON old.id = projected.legacy_permission_id
|
||||
OR (
|
||||
projected.legacy_permission_id IS NULL
|
||||
AND old.workspace_id = projected.workspace_id
|
||||
AND old.user_id = projected.invitee_user_id
|
||||
AND old.status <> 'Accepted'::"WorkspaceMemberStatus"
|
||||
)
|
||||
WHERE projected.invitee_user_id IS NOT NULL
|
||||
AND (
|
||||
old.id IS NULL OR
|
||||
old.status = 'Accepted'::"WorkspaceMemberStatus" OR
|
||||
affine_permission_workspace_invitation_state(old.status) IS NULL OR
|
||||
affine_permission_legacy_workspace_role(old.type) IS NULL
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_page_user_permissions old
|
||||
LEFT JOIN doc_grants projected
|
||||
ON projected.workspace_id = old.workspace_id
|
||||
AND projected.doc_id = old.page_id
|
||||
AND projected.principal_type = 'user'
|
||||
AND projected.principal_id = old.user_id
|
||||
WHERE affine_permission_legacy_doc_role(old.type) IS NOT NULL
|
||||
AND (
|
||||
projected.workspace_id IS NULL OR
|
||||
projected.role <> affine_permission_legacy_doc_role(old.type)
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM doc_grants projected
|
||||
LEFT JOIN workspace_page_user_permissions old
|
||||
ON old.workspace_id = projected.workspace_id
|
||||
AND old.page_id = projected.doc_id
|
||||
AND old.user_id = projected.principal_id
|
||||
WHERE projected.principal_type = 'user'
|
||||
AND (
|
||||
old.workspace_id IS NULL OR
|
||||
affine_permission_legacy_doc_role(old.type) IS NULL
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM workspace_pages old
|
||||
LEFT JOIN doc_access_policies projected
|
||||
ON projected.workspace_id = old.workspace_id
|
||||
AND projected.doc_id = old.page_id
|
||||
WHERE affine_permission_legacy_default_doc_role(old."defaultRole") IS NOT NULL
|
||||
AND (
|
||||
projected.workspace_id IS NULL OR
|
||||
projected.visibility <> CASE WHEN old.public THEN 'public' ELSE 'private' END OR
|
||||
projected.public_role IS DISTINCT FROM CASE WHEN old.public THEN 'external' ELSE NULL END OR
|
||||
projected.member_default_role IS DISTINCT FROM affine_permission_legacy_default_doc_role(old."defaultRole")
|
||||
)
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS count
|
||||
FROM doc_access_policies projected
|
||||
LEFT JOIN workspace_pages old
|
||||
ON old.workspace_id = projected.workspace_id
|
||||
AND old.page_id = projected.doc_id
|
||||
WHERE old.workspace_id IS NULL
|
||||
`),
|
||||
count(this.db.$queryRaw<CountRow[]>`
|
||||
SELECT COALESCE(SUM(conflicts.count - 1), 0)::bigint AS count
|
||||
FROM (
|
||||
SELECT workspace_id, COUNT(*)::bigint AS count
|
||||
FROM workspace_members
|
||||
WHERE state = 'active'
|
||||
AND role = 'owner'
|
||||
GROUP BY workspace_id
|
||||
HAVING COUNT(*) > 1
|
||||
UNION ALL
|
||||
SELECT workspace_id || ':' || doc_id AS workspace_id, COUNT(*)::bigint AS count
|
||||
FROM doc_grants
|
||||
WHERE principal_type = 'user'
|
||||
AND role = 'owner'
|
||||
GROUP BY workspace_id, doc_id
|
||||
HAVING COUNT(*) > 1
|
||||
) conflicts
|
||||
`),
|
||||
this.db.$queryRaw<ProjectionIssueRow[]>`
|
||||
SELECT category, COUNT(*)::bigint AS count
|
||||
FROM (
|
||||
SELECT 'unknown_workspace_role' AS category
|
||||
FROM workspace_user_permissions
|
||||
WHERE affine_permission_legacy_workspace_role(type) IS NULL
|
||||
AND type <> -99
|
||||
UNION ALL
|
||||
SELECT 'unknown_doc_role' AS category
|
||||
FROM workspace_page_user_permissions
|
||||
WHERE affine_permission_legacy_doc_role(type) IS NULL
|
||||
AND type NOT IN (0, -32768)
|
||||
UNION ALL
|
||||
SELECT 'legacy_doc_external_row' AS category
|
||||
FROM workspace_page_user_permissions
|
||||
WHERE type = 0
|
||||
UNION ALL
|
||||
SELECT 'legacy_doc_none_row' AS category
|
||||
FROM workspace_page_user_permissions
|
||||
WHERE type = -32768
|
||||
UNION ALL
|
||||
SELECT 'doc_default_owner' AS category
|
||||
FROM workspace_pages
|
||||
WHERE "defaultRole" = 99
|
||||
) issues
|
||||
GROUP BY category
|
||||
`,
|
||||
]);
|
||||
|
||||
return {
|
||||
oldWorkspacePolicyMismatch,
|
||||
oldAcceptedMemberMismatch,
|
||||
extraProjectedMember,
|
||||
oldInvitationMismatch,
|
||||
extraProjectedInvitation,
|
||||
oldDocGrantMismatch,
|
||||
extraProjectedDocGrant,
|
||||
oldDocPolicyMismatch,
|
||||
extraProjectedDocPolicy,
|
||||
runtimeStateMissing: 0,
|
||||
runtimeStateMismatch: 0,
|
||||
ownerConflict,
|
||||
oldNewDecisionMismatch: 0,
|
||||
invalidLegacyRows: Object.fromEntries(
|
||||
invalidLegacyRows.map(row => [row.category, Number(row.count)])
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async lockWorkspaceOwnerTransfer(workspaceId: string) {
|
||||
await this.db.$executeRaw`
|
||||
SELECT pg_advisory_xact_lock(hashtextextended(${workspaceId}, 16))
|
||||
`;
|
||||
}
|
||||
|
||||
async lockDocOwnerTransfer(workspaceId: string, docId: string) {
|
||||
await this.db.$executeRaw`
|
||||
SELECT pg_advisory_xact_lock(hashtextextended(${`${workspaceId}:${docId}`}, 16))
|
||||
`;
|
||||
}
|
||||
|
||||
async markNewWriteOrigin() {
|
||||
await this.db.$executeRaw`
|
||||
SELECT set_config('affine.permission_sync_origin', 'new', true)
|
||||
`;
|
||||
}
|
||||
|
||||
async markLegacyWriteOrigin() {
|
||||
await this.db.$executeRaw`
|
||||
SELECT set_config('affine.permission_sync_origin', 'legacy', true)
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user