Skip to content

Commit

Permalink
Merge pull request #1713 from quadratichq/qa
Browse files Browse the repository at this point in the history
QA: Aug 13th
  • Loading branch information
davidkircos authored Aug 22, 2024
2 parents b0ac880 + 978df35 commit d560fbe
Show file tree
Hide file tree
Showing 288 changed files with 15,210 additions and 2,383 deletions.
199 changes: 106 additions & 93 deletions Cargo.lock

Large diffs are not rendered by default.

394 changes: 378 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion quadratic-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@types/node": "^18.11.9",
"@types/supertest": "^2.0.12",
"auth0": "^3.6.1",
"axios": "^1.5.0",
"axios": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.19.2",
"express-async-errors": "^3.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Team" ADD COLUMN "client_data_kv" JSONB NOT NULL DEFAULT '{}';
5 changes: 5 additions & 0 deletions quadratic-api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ model Team {
stripeCurrentPeriodEnd DateTime? @map("stripe_current_period_end")
stripeSubscriptionLastUpdated DateTime? @map("stripe_subscription_last_updated")
// Key/value storage used by Quadratic client on the Team.
// It remembers things like closing the onboarding banner.
// Use for client-specific data that is not useful to the server or other services
clientDataKv Json @default("{}") @map("client_data_kv")
@@index([uuid])
}

Expand Down
93 changes: 51 additions & 42 deletions quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,35 @@ import { User } from 'auth0';
import request from 'supertest';
import { app } from '../../app';
import dbClient from '../../dbClient';
import { clearDb } from '../../tests/testDataGenerator';
import { clearDb, createFile, createTeam, createUser } from '../../tests/testDataGenerator';

beforeEach(async () => {
// Create some users & team
const user_1 = await dbClient.user.create({
data: {
auth0Id: 'team_1_owner',
},
});
const user_2 = await dbClient.user.create({
data: {
auth0Id: 'team_1_editor',
},
});
const user_3 = await dbClient.user.create({
data: {
auth0Id: 'team_1_viewer',
},
});
await dbClient.user.create({
data: {
auth0Id: 'user_without_team',
},
});
await dbClient.team.create({
data: {
const user_1 = await createUser({ auth0Id: 'team_1_owner' });
const user_2 = await createUser({ auth0Id: 'team_1_editor' });
const user_3 = await createUser({ auth0Id: 'team_1_viewer' });
await createUser({ auth0Id: 'user_without_team' });

const team = await createTeam({
team: {
name: 'Test Team 1',
uuid: '00000000-0000-4000-8000-000000000001',
UserTeamRole: {
create: [
{
userId: user_1.id,
role: 'OWNER',
},
{ userId: user_2.id, role: 'EDITOR' },
{ userId: user_3.id, role: 'VIEWER' },
],
},
users: [
{
userId: user_1.id,
role: 'OWNER',
},
{ userId: user_2.id, role: 'EDITOR' },
{ userId: user_3.id, role: 'VIEWER' },
],
connections: [{ type: 'POSTGRES' }],
});

await createFile({
data: {
name: 'Test File 1',
ownerTeamId: team.id,
creatorUserId: user_1.id,
},
});
});
Expand Down Expand Up @@ -95,14 +86,6 @@ jest.mock('auth0', () => {
});

describe('GET /v0/teams/:uuid', () => {
// TODO the auth/team middleware should handle all this...?
describe('sending a bad request', () => {
it.todo('responds with a 401 without authentication');
it.todo('responds with a 404 for requesting a team that doesn’t exist');
it.todo('responds with a 400 for failing schema validation on the team UUID');
it.todo('responds with a 400 for failing schema validation on the payload');
});

describe('get a team you belong to', () => {
// TODO different responses for OWNER, EDITOR, VIEWER?
it('responds with a team', async () => {
Expand All @@ -113,7 +96,11 @@ describe('GET /v0/teams/:uuid', () => {
.expect((res) => {
expect(res.body).toHaveProperty('team');
expect(res.body.team.uuid).toBe('00000000-0000-4000-8000-000000000001');
expect(res.body.team.name).toBe('Test Team 1');

expect(res.body.clientDataKv).toStrictEqual({});
expect(res.body.connections).toHaveLength(1);
expect(res.body.files).toHaveLength(1);
expect(typeof res.body.files[0].file.creatorId).toBe('number');

expect(res.body.users[0].email).toBe('[email protected]');
expect(res.body.users[0].role).toBe('OWNER');
Expand All @@ -128,5 +115,27 @@ describe('GET /v0/teams/:uuid', () => {
expect(res.body.users[2].name).toBe('Test User 3');
});
});

it('does not return archived connections', async () => {
// delete all connections in a team
const team = await dbClient.team.findUniqueOrThrow({
where: {
uuid: '00000000-0000-4000-8000-000000000001',
},
});
await dbClient.connection.deleteMany({
where: {
teamId: team.id,
},
});

await request(app)
.get(`/v0/teams/00000000-0000-4000-8000-000000000001`)
.set('Authorization', `Bearer ValidToken team_1_owner`)
.expect(200)
.expect((res) => {
expect(res.body.connections).toHaveLength(0);
});
});
});
});
15 changes: 15 additions & 0 deletions quadratic-api/src/routes/v0/teams.$uuid.GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
},
include: {
Connection: {
where: {
archived: null,
},
orderBy: {
createdDate: 'desc',
},
Expand Down Expand Up @@ -135,6 +138,7 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
updatedDate: file.updatedDate.toISOString(),
publicLinkAccess: file.publicLinkAccess,
thumbnail: file.thumbnail,
creatorId: file.creatorUserId,
},
userMakingRequest: {
filePermissions: getFilePermissions({
Expand Down Expand Up @@ -168,7 +172,18 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
}),
},
})),
connections: dbTeam.Connection.map((connection) => ({
uuid: connection.uuid,
name: connection.name,
createdDate: connection.createdDate.toISOString(),
type: connection.type,
})),
clientDataKv: isObject(dbTeam.clientDataKv) ? dbTeam.clientDataKv : {},
};

return res.status(200).json(response);
}

function isObject(x: any): x is Record<string, any> {
return typeof x === 'object' && !Array.isArray(x) && x !== null;
}
33 changes: 14 additions & 19 deletions quadratic-api/src/routes/v0/teams.$uuid.PATCH.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('PATCH /v0/teams/:uuid', () => {
});

describe('update a team', () => {
it('accepts change from OWNER', async () => {
it('accepts name change', async () => {
await request(app)
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
.send({ name: 'Foobar' })
Expand All @@ -82,32 +82,27 @@ describe('PATCH /v0/teams/:uuid', () => {
expect(res.body.name).toBe('Foobar');
});
});

it('accepts change from EDITOR', async () => {
it('accepst key/value pair updates', async () => {
// Create value
await request(app)
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
.send({ name: 'Foobar' })
.set('Authorization', `Bearer ValidToken team_1_editor`)
.send({ clientDataKv: { foo: 'bar' } })
.set('Authorization', `Bearer ValidToken team_1_owner`)
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('Foobar');
expect(res.body.clientDataKv.foo).toBe('bar');
});
});

it('rejects change from VIEWER', async () => {
// Update value
await request(app)
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
.send({ name: 'Foobar' })
.set('Authorization', `Bearer ValidToken team_1_viewer`)
.expect(403);
});

it('rejects change from someone who isn’t a team member', async () => {
await request(app)
.patch(`/v0/teams/00000000-0000-4000-8000-000000000001`)
.send({ name: 'Foobar' })
.set('Authorization', `Bearer ValidToken no_team`)
.expect(403);
.send({ clientDataKv: { anotherValue: 'hello' } })
.set('Authorization', `Bearer ValidToken team_1_owner`)
.expect(200)
.expect((res) => {
expect(res.body.clientDataKv.foo).toBe('bar');
expect(res.body.clientDataKv.anotherValue).toBe('hello');
});
});
});
});
37 changes: 27 additions & 10 deletions quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Response } from 'express';
import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas';
import { ApiSchemas, ApiTypes, TeamClientDataKvSchema } from 'quadratic-shared/typesAndSchemas';
import z from 'zod';
import dbClient from '../../dbClient';
import { getTeam } from '../../middleware/getTeam';
import { userMiddleware } from '../../middleware/user';
import { validateAccessToken } from '../../middleware/validateAccessToken';
import { parseRequest } from '../../middleware/validateRequestSchema';
import { updateCustomer } from '../../stripe/stripe';
import { RequestWithUser } from '../../types/Request';
import { ApiError } from '../../utils/ApiError';

Expand All @@ -21,35 +20,53 @@ const schema = z.object({

async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:uuid.PATCH.response']>) {
const {
body: { name },
body: { name, clientDataKv },
params: { uuid },
} = parseRequest(req, schema);
const {
user: { id: userId },
} = req;
const {
userMakingRequest: { permissions },
team: { stripeCustomerId },
team: { clientDataKv: exisitingClientDataKv },
} = await getTeam({ uuid, userId });

// Can the user even edit this team?
if (!permissions.includes('TEAM_EDIT')) {
throw new ApiError(403, 'User does not have permission to edit this team.');
}

// Update Customer name in Stripe
if (stripeCustomerId) {
await updateCustomer(stripeCustomerId, name);
// Have they supplied _something_?
if (!name && !clientDataKv) {
throw new ApiError(400, '`name` or `clientDataKv` are required');
}

// Update the team name
// Validate exisiting data in the db
const validatedExisitingClientDataKv = validateClientDataKv(exisitingClientDataKv);

// Update the team with supplied data
const newTeam = await dbClient.team.update({
where: {
uuid,
},
data: {
name,
...(name ? { name } : {}),
...(clientDataKv ? { clientDataKv: { ...validatedExisitingClientDataKv, ...clientDataKv } } : {}),
},
});
return res.status(200).json({ name: newTeam.name });

// Return the new data
const newClientDataKv = validateClientDataKv(newTeam.clientDataKv);
return res.status(200).json({
name: newTeam.name,
clientDataKv: newClientDataKv,
});
}

function validateClientDataKv(clientDataKv: unknown) {
const parseResult = TeamClientDataKvSchema.safeParse(clientDataKv);
if (!parseResult.success) {
throw new ApiError(500, '`clientDataKv` must be a valid JSON object');
}
return parseResult.data;
}
5 changes: 5 additions & 0 deletions quadratic-client/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"shadcn"
]
}
1 change: 1 addition & 0 deletions quadratic-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@monaco-editor/react": "^4.3.1",
"@mui/icons-material": "^5.2.0",
"@mui/material": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added quadratic-client/public/images/checkbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added quadratic-client/public/images/dropdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions quadratic-client/scripts/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const fontDirectory = path.join('public', 'fonts');
const fontFamilies = ['opensans'];
const fontFiles = ['OpenSans', 'OpenSans-Bold', 'OpenSans-Italic', 'OpenSans-BoldItalic'];

const charSet = [''];

async function cleanOldFiles() {
console.log('Removing old files...');
for (const family of fontFamilies) {
Expand Down
18 changes: 14 additions & 4 deletions quadratic-client/src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider';
import { ROUTES } from '@/shared/constants/routes';
import { DOCUMENTATION_URL } from '@/shared/constants/urls';
import { ApiTypes, FilePermission, FilePermissionSchema, TeamPermission } from 'quadratic-shared/typesAndSchemas';
import { NavigateFunction, SubmitFunction } from 'react-router-dom';
import { SubmitFunction } from 'react-router-dom';
import { SetterOrUpdater } from 'recoil';
const { FILE_EDIT, FILE_DELETE } = FilePermissionSchema.enum;

Expand Down Expand Up @@ -70,10 +70,10 @@ export const isAvailableBecauseFileLocationIsAccessibleAndWriteable = ({
}: IsAvailableArgs) => Boolean(fileTeamPrivacy) && Boolean(teamPermissions?.includes('TEAM_EDIT'));

export const createNewFileAction = {
label: 'Create',
label: 'New',
isAvailable: isAvailableBecauseFileLocationIsAccessibleAndWriteable,
run({ navigate, teamUuid }: { navigate: NavigateFunction; teamUuid: string }) {
navigate(ROUTES.CREATE_FILE_PRIVATE(teamUuid));
run({ setEditorInteractionState }: { setEditorInteractionState: SetterOrUpdater<EditorInteractionState> }) {
setEditorInteractionState((prevState) => ({ ...prevState, showNewFileMenu: true }));
},
};

Expand Down Expand Up @@ -186,6 +186,11 @@ export const downloadSelectionAsCsvAction = {
},
};

export const dataValidations = {
label: 'Data Validations',
isAvailable: isAvailableBecauseCanEditFile,
};

export const findInSheet = {
label: 'Find in current sheet',
};
Expand All @@ -197,3 +202,8 @@ export const resizeColumnAction = {
label: 'Resize column to fit data',
isAvailable: isAvailableBecauseCanEditFile,
};

export const validationAction = {
label: 'Data Validations',
isAvailable: isAvailableBecauseCanEditFile,
};
Loading

0 comments on commit d560fbe

Please sign in to comment.