Skip to content

Commit

Permalink
Migrate project to new data model class.
Browse files Browse the repository at this point in the history
  • Loading branch information
romain-growthbook committed May 7, 2024
1 parent bba9bb7 commit 21ae92e
Show file tree
Hide file tree
Showing 17 changed files with 105 additions and 162 deletions.
3 changes: 1 addition & 2 deletions packages/back-end/src/api/bulk-import/postBulkImportFacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
updateFactFilter,
getFactTableMap,
} from "../../models/FactTableModel";
import { findAllProjectsByOrganization } from "../../models/ProjectModel";
import { createApiRequestHandler } from "../../util/handler";
import { postBulkImportFactsValidator } from "../../validators/openapi";
import { getCreateMetricPropsFromBody } from "../fact-metrics/postFactMetric";
Expand Down Expand Up @@ -48,7 +47,7 @@ export const postBulkImportFacts = createApiRequestHandler(

const tagsToAdd = new Set<string>();

const projects = await findAllProjectsByOrganization(req.context);
const projects = await req.context.models.projects.getAll();
const projectIds = new Set(projects.map((p) => p.id));
function validateProjectIds(ids: string[]) {
for (const id of ids) {
Expand Down
3 changes: 1 addition & 2 deletions packages/back-end/src/api/fact-tables/postFactTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createFactTable,
toFactTableApiInterface,
} from "../../models/FactTableModel";
import { findAllProjectsByOrganization } from "../../models/ProjectModel";
import { addTags } from "../../models/TagModel";
import { createApiRequestHandler } from "../../util/handler";
import { postFactTableValidator } from "../../validators/openapi";
Expand Down Expand Up @@ -37,7 +36,7 @@ export const postFactTable = createApiRequestHandler(postFactTableValidator)(

// Validate projects
if (req.body.projects?.length) {
const projects = await findAllProjectsByOrganization(req.context);
const projects = await req.context.models.projects.getAll();
const projectIds = new Set(projects.map((p) => p.id));
for (const projectId of req.body.projects) {
if (!projectIds.has(projectId)) {
Expand Down
3 changes: 1 addition & 2 deletions packages/back-end/src/api/fact-tables/updateFactTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
toFactTableApiInterface,
getFactTable,
} from "../../models/FactTableModel";
import { findAllProjectsByOrganization } from "../../models/ProjectModel";
import { addTagsDiff } from "../../models/TagModel";
import { createApiRequestHandler } from "../../util/handler";
import { updateFactTableValidator } from "../../validators/openapi";
Expand All @@ -27,7 +26,7 @@ export const updateFactTable = createApiRequestHandler(

// Validate projects
if (req.body.projects?.length) {
const projects = await findAllProjectsByOrganization(req.context);
const projects = await req.context.models.projects.getAll();
const projectIds = new Set(projects.map((p) => p.id));
for (const projectId of req.body.projects) {
if (!projectIds.has(projectId)) {
Expand Down
7 changes: 2 additions & 5 deletions packages/back-end/src/api/projects/getProject.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { GetProjectResponse } from "../../../types/openapi";
import {
findProjectById,
toProjectApiInterface,
} from "../../models/ProjectModel";
import { toProjectApiInterface } from "../../models/ProjectModel";
import { createApiRequestHandler } from "../../util/handler";
import { getProjectValidator } from "../../validators/openapi";

export const getProject = createApiRequestHandler(getProjectValidator)(
async (req): Promise<GetProjectResponse> => {
const project = await findProjectById(req.context, req.params.id);
const project = await req.context.models.projects.getById(req.params.id);
if (!project) {
throw new Error("Could not find project with that id");
}
Expand Down
7 changes: 2 additions & 5 deletions packages/back-end/src/api/projects/listProjects.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { ListProjectsResponse } from "../../../types/openapi";
import {
findAllProjectsByOrganization,
toProjectApiInterface,
} from "../../models/ProjectModel";
import { toProjectApiInterface } from "../../models/ProjectModel";
import { applyPagination, createApiRequestHandler } from "../../util/handler";
import { listProjectsValidator } from "../../validators/openapi";

export const listProjects = createApiRequestHandler(listProjectsValidator)(
async (req): Promise<ListProjectsResponse> => {
const projects = await findAllProjectsByOrganization(req.context);
const projects = await req.context.models.projects.getAll();

// TODO: Move sorting/limiting to the database query for better performance
const { filtered, returnFields } = applyPagination(
Expand Down
10 changes: 3 additions & 7 deletions packages/back-end/src/controllers/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ import {
import { VisualChangesetInterface } from "../../types/visual-changeset";
import { ApiReqContext, PrivateApiErrorResponse } from "../../types/api";
import { EventAuditUserForResponseLocals } from "../events/event-types";
import {
findAllProjectsByOrganization,
findProjectById,
} from "../models/ProjectModel";
import { ExperimentResultsQueryRunner } from "../queryRunners/ExperimentResultsQueryRunner";
import { PastExperimentsQueryRunner } from "../queryRunners/PastExperimentsQueryRunner";
import {
Expand Down Expand Up @@ -137,7 +133,7 @@ export async function getExperimentsFrequencyMonth(
project = req.query.project;
}

const allProjects = await findAllProjectsByOrganization(context);
const allProjects = await context.models.projects.getAll();
const { num } = req.params;
const experiments = await getAllExperiments(context, project);

Expand Down Expand Up @@ -1747,7 +1743,7 @@ async function createExperimentSnapshot({
}) {
let project = null;
if (experiment.project) {
project = await findProjectById(context, experiment.project);
project = await context.models.projects.getById(experiment.project);
}

const { org } = context;
Expand Down Expand Up @@ -1867,7 +1863,7 @@ export async function postSnapshot(

let project = null;
if (experiment.project) {
project = await findProjectById(context, experiment.project);
project = await context.models.projects.getById(experiment.project);
}
const { settings } = getScopedSettings({
organization: org,
Expand Down
3 changes: 1 addition & 2 deletions packages/back-end/src/jobs/updateExperimentResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { notifyAutoUpdate } from "../services/experimentNotifications";
import { EXPERIMENT_REFRESH_FREQUENCY } from "../util/secrets";
import { logger } from "../util/logger";
import { ExperimentSnapshotInterface } from "../../types/experiment-snapshot";
import { findProjectById } from "../models/ProjectModel";
import { getExperimentWatchers } from "../models/WatchModel";
import { getFactTableMap } from "../models/FactTableModel";
import { ApiReqContext } from "../../types/api";
Expand Down Expand Up @@ -126,7 +125,7 @@ async function updateSingleExperiment(job: UpdateSingleExpJob) {

let project = null;
if (experiment.project) {
project = await findProjectById(context, experiment.project);
project = await context.models.projects.getById(experiment.project);
}
const { settings: scopedSettings } = getScopedSettings({
organization: context.org,
Expand Down
152 changes: 48 additions & 104 deletions packages/back-end/src/models/ProjectModel.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,69 @@
import mongoose from "mongoose";
import uniqid from "uniqid";
import { DEFAULT_STATS_ENGINE } from "shared/constants";
import { omit } from "lodash";
import { hasReadAccess } from "shared/permissions";
import { z } from "zod";
import { ApiProject } from "../../types/openapi";
import { ProjectInterface, ProjectSettings } from "../../types/project";
import { ReqContext } from "../../types/organization";
import { ApiReqContext } from "../../types/api";
import { statsEngines } from "../util/constants";
import { baseSchema, MakeModelClass } from "./BaseModel";

const projectSchema = new mongoose.Schema({
id: {
type: String,
unique: true,
},
organization: {
type: String,
index: true,
const projectValidator = baseSchema
.extend({
name: z.string(),
description: z.string(),
settings: z.object({
statsEngine: z.enum(statsEngines),
}),
})
.strict();

const BaseClass = MakeModelClass({
schema: projectValidator,
collectionName: "projects",
idPrefix: "prj__",
auditLog: {
entity: "project",
createEvent: "project.create",
updateEvent: "project.update",
deleteEvent: "project.delete",
},
name: String,
description: String,
dateCreated: Date,
dateUpdated: Date,
settings: {},
projectScoping: "none",
globallyUniqueIds: true,
});

type ProjectDocument = mongoose.Document & ProjectInterface;

const ProjectModel = mongoose.model<ProjectInterface>("Project", projectSchema);

function toInterface(doc: ProjectDocument): ProjectInterface {
const ret = doc.toJSON<ProjectDocument>();
ret.settings = ret.settings || {};
return omit(ret, ["__v", "_id"]);
}

interface CreateProjectProps {
name: string;
description?: string;
id?: string;
}

export async function createProject(
organization: string,
data: CreateProjectProps
) {
const doc = await ProjectModel.create({
organization: organization,
id: data.id || uniqid("prj_"),
name: data.name || "",
description: data.description,
dateCreated: new Date(),
dateUpdated: new Date(),
});
return toInterface(doc);
}
export async function findAllProjectsByOrganization(
context: ReqContext | ApiReqContext
) {
const { org, readAccessFilter } = context;
const docs = await ProjectModel.find({
organization: org.id,
});
export class ProjectModel extends BaseClass {
protected canRead(doc: ProjectInterface) {
return hasReadAccess(this.context.readAccessFilter, doc.id);
}

const projects = docs.map(toInterface);
return projects.filter((p) => hasReadAccess(readAccessFilter, p.id));
}
export async function findProjectById(
context: ReqContext | ApiReqContext,
projectId: string
) {
const { org, readAccessFilter } = context;
const doc = await ProjectModel.findOne({
id: projectId,
organization: org.id,
});
if (!doc) return null;
protected canCreate() {
return this.context.permissions.canCreateProjects();
}

const project = toInterface(doc);
protected canUpdate(doc: ProjectInterface) {
return this.context.permissions.canUpdateProject(doc.id);
}

return hasReadAccess(readAccessFilter, project.id) ? project : null;
}
export async function deleteProjectById(id: string, organization: string) {
await ProjectModel.deleteOne({
id,
organization,
});
}
export async function updateProject(
id: string,
organization: string,
update: Partial<ProjectInterface>
) {
await ProjectModel.updateOne(
{
id,
organization,
},
{
$set: update,
}
);
}
protected canDelete(doc: ProjectInterface) {
return this.context.permissions.canDeleteProject(doc.id);
}

export async function updateProjectSettings(
id: string,
organization: string,
settings: Partial<ProjectSettings>
) {
const update = {
$set: {
dateUpdated: new Date(),
settings,
},
};
await ProjectModel.updateOne(
{
id,
organization,
},
update
);
protected migrate(doc: z.infer<typeof projectValidator>) {
return { ...doc, settings: doc.settings || {} };
}

public create(project: CreateProjectProps) {
return super.create({ ...project, settings: {} });
}

public updateSettingsById(id: string, settings: Partial<ProjectSettings>) {
return super.updateById(id, { settings });
}
}

export function toProjectApiInterface(project: ProjectInterface): ApiProject {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
createExperiment,
getAllExperiments,
} from "../../models/ExperimentModel";
import { createProject, findProjectById } from "../../models/ProjectModel";
import { createMetric, createSnapshot } from "../../services/experiments";
import { PrivateApiErrorResponse } from "../../../types/api";
import { DataSourceSettings } from "../../../types/datasource";
Expand Down Expand Up @@ -184,8 +183,7 @@ export const postDemoDatasourceProject = async (
context.permissions.throwPermissionError();
}

const existingDemoProject: ProjectInterface | null = await findProjectById(
context,
const existingDemoProject: ProjectInterface | null = await context.models.projects.getById(
demoProjId
);

Expand All @@ -204,7 +202,7 @@ export const postDemoDatasourceProject = async (
}

try {
const project = await createProject(org.id, {
const project = await context.models.projects.create({
id: demoProjId,
name: "Sample Data",
});
Expand Down
Loading

0 comments on commit 21ae92e

Please sign in to comment.