Files
cal-diy-oidc/scripts/prepare-local-for-delegation-credentials-testing.js
Eunjae Lee f4248bf20d feat: implement FeatureOptInService (#25805)
* feat: implement FeatureOptInService WIP

* clean up

* feat: consolidate feature repositories and add updateFeatureForUser

- Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam)
- Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository
- Update FeatureOptInService to use only FeaturesRepository
- Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService
- Update _router.ts to remove PrismaFeatureOptInRepository usage
- Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts
- Update features.repository.interface.ts and features.repository.mock.ts
- Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState
- Update service.integration-test.ts to use FeaturesRepository

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename updateFeatureForUser to setUserFeatureState

Rename to match the convention used for setTeamFeatureState

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState

* fix integration tests

* clean up logics

* update services and router

* refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array

- Renamed getUserFeatureState to getUserFeatureStates
- Renamed getTeamFeatureState to getTeamFeatureStates
- Changed parameter from featureId: string to featureIds: string[]
- Changed return type from FeatureState to Record<string, FeatureState>
- Updated FeatureOptInService to use the new batch methods
- Added tests for querying multiple features in a single call
- Optimized listFeaturesForTeam to fetch all feature states in one query

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* feat: add getFeatureStateForTeams for batch querying multiple teams

- Added getFeatureStateForTeams method to query a single feature across multiple teams in one call
- Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method
- Replaces N+1 queries with a single database query for team states
- Added comprehensive integration tests for the new method

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: combine org and team state queries into single call

- Include orgId in the teamIds array passed to getFeatureStateForTeams
- Extract org state and team states from the combined result
- Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use team.isOrganization and clarify computeEffectiveState comment

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use MembershipRepository.findAllByUserId with isOrganization

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* feat: add featureId validation using isOptInFeature type guard

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* less queries

* add fallback value

* fix type error

* move files

* add autoOptInFeatures column

* use autoOptInFeatures flag within FeatureOptInService

* add setUserAutoOptIn and setTeamAutoOptIn

* fix computeEffectiveState logic

* rewrite computeEffectiveState

* clean up integration tests

* clean up in afterEach

* fix type error

* refactor: use FeaturesRepository methods instead of direct Prisma calls

Replace all manual userFeatures and teamFeatures Prisma operations with
the new setUserFeatureState and setTeamFeatureState repository methods.

Changes include:
- Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam)
- Test fixtures and integration tests
- Playwright fixtures
- Development scripts

This ensures consistent feature flag management through the repository
pattern and supports the new tri-state semantics (enabled/disabled/inherit).

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* clean up

* fix the logic

* extract some logic into applyAutoOptIn()

* remove wrong code

* refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union

- Convert multiple positional parameters to single object parameter
- Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit
- Update all callers across repository, service, handlers, fixtures, and tests

* fix type error

* use Promise.all

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-06 16:55:53 +01:00

143 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
/**
* This script is used to prepare local environment for delegation credentials testing.
* It prepares Acme organization and its owner user with email owner1-acme@example.com to test Delegation Credentials with Calendar Cache
*/
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function main() {
// Dynamic import for ES module
const { FeaturesRepository } = await import("@calcom/features/flags/features.repository");
const featuresRepository = new FeaturesRepository(prisma);
// Parse newEmail from args
const newEmail = process.argv[2] || "hariom@cal.com";
console.log(`Using newEmail: ${newEmail}`);
// 1. Update user email
let user = await prisma.user.findUnique({
where: { email: "owner1-acme@example.com" },
});
if (!user) {
// Check if user with newEmail exists
user = await prisma.user.findUnique({ where: { email: newEmail } });
if (user) {
console.log(`User with newEmail (${newEmail}) already exists. Skipping email update step.`);
} else {
console.error(
"User with email owner1-acme@example.com not found, and user with newEmail also not found."
);
process.exit(1);
}
} else {
if (user.email !== newEmail) {
await prisma.user.update({
where: { id: user.id },
data: { email: newEmail },
});
console.log(`Updated user email to ${newEmail}`);
} else {
console.log("User email already set to newEmail, skipping update.");
}
}
// 2. Find organization (Team)
const org = await prisma.team.findFirst({
where: { slug: "acme", isOrganization: true },
});
if (!org) {
console.error("Organization (Team) with slug=acme and isOrganization=true not found.");
process.exit(1);
}
console.log(`Found organization: id=${org.id}, slug=${org.slug}`);
// 3. Ensure TeamFeatures: delegation-credential
const delegationFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: org.id,
featureId: "delegation-credential",
},
enabled: true,
},
});
if (!delegationFeature) {
await featuresRepository.setTeamFeatureState({
teamId: org.id,
featureId: "delegation-credential",
state: "enabled",
assignedBy: "prepare-local-script",
});
console.log("Created TeamFeatures: delegation-credential");
} else {
console.log("TeamFeatures: delegation-credential already exists, skipping.");
}
// 4. Ensure TeamFeatures: calendar-cache
const calendarCacheFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: org.id,
featureId: "calendar-cache",
},
enabled: true,
},
});
if (!calendarCacheFeature) {
await featuresRepository.setTeamFeatureState({
teamId: org.id,
featureId: "calendar-cache",
state: "enabled",
assignedBy: "prepare-local-script",
});
console.log("Created TeamFeatures: calendar-cache");
} else {
console.log("TeamFeatures: calendar-cache already exists, skipping.");
}
// 5. Add WorkspacePlatform record
const workspacePlatform = await prisma.workspacePlatform.findUnique({
where: { slug: "google" },
});
if (!workspacePlatform) {
await prisma.workspacePlatform.create({
data: {
slug: "google",
name: "Google",
enabled: true,
description: "Google Workspace Platform",
defaultServiceAccountKey: {}, // Empty object, update as needed
},
});
console.log("Created WorkspacePlatform: google");
} else {
console.log("WorkspacePlatform: google already exists, skipping.");
}
// 6. Enable Feature records for 'calendar-cache' and 'delegation-credential'
const featureSlugs = ["calendar-cache", "delegation-credential"];
for (const slug of featureSlugs) {
const feature = await prisma.feature.findUnique({ where: { slug } });
if (!feature) {
console.error(`Feature with slug ${slug} not found.`);
process.exit(1);
}
if (!feature.enabled) {
await prisma.feature.update({ where: { slug }, data: { enabled: true } });
console.log(`Enabled Feature: ${slug}`);
} else {
console.log(`Feature: ${slug} already enabled, skipping.`);
}
}
console.log(`Now you can sign in with ${newEmail} and create a new Delegation Credential.`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});