Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/questions supabase #4053

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e02543f
chore: add tenant_id to cypress environment
mariojsnunes Dec 10, 2024
1d508aa
chore: seed database
mariojsnunes Dec 10, 2024
4df42f0
wip questions
mariojsnunes Dec 10, 2024
e3d8b6a
wip questions
mariojsnunes Dec 17, 2024
eb170c5
Merge branch 'master' into feat/questions-supabase
mariojsnunes Dec 20, 2024
69912aa
feat: questions supabase
mariojsnunes Dec 28, 2024
ee6c71d
fix: discussions with supabase questions
mariojsnunes Dec 29, 2024
e58c5f8
fix: notifications with supabase questions;
mariojsnunes Dec 29, 2024
09139cd
feat: image public url and transforms
mariojsnunes Jan 3, 2025
9790968
fix: questions list
mariojsnunes Jan 4, 2025
0469958
merge
mariojsnunes Jan 4, 2025
c8c306a
feat: added sort by comments
mariojsnunes Jan 6, 2025
f42a0a3
fix import
mariojsnunes Jan 7, 2025
d73ce48
questions migration script
mariojsnunes Jan 7, 2025
758b146
Merge branch 'master' into feat/questions-supabase
mariojsnunes Jan 9, 2025
cee0ed1
merge
mariojsnunes Jan 9, 2025
9a5f00a
remove guidance for files
mariojsnunes Jan 9, 2025
3b78489
remove question store
mariojsnunes Jan 9, 2025
27037b2
remove question store
mariojsnunes Jan 9, 2025
35390e5
feat: duplicate question validation and error message
mariojsnunes Jan 9, 2025
43ae242
fix: spec for file category guidance
mariojsnunes Jan 9, 2025
a9cfe8f
Merge branch 'chore/comments-v2-tests' into feat/questions-supabase
mariojsnunes Jan 9, 2025
9beeb20
chore: updated cypress tests with supabase
mariojsnunes Jan 9, 2025
476f89c
chore: update migration script;
mariojsnunes Jan 9, 2025
f71fc3d
fix
mariojsnunes Jan 10, 2025
c08417e
remove unique check
mariojsnunes Jan 10, 2025
2491f44
chore: fix tests to work with supabase;
mariojsnunes Jan 11, 2025
56e8293
fix: edit question submit button
mariojsnunes Jan 11, 2025
be11d96
fix
mariojsnunes Jan 11, 2025
f38567c
fix: library category
mariojsnunes Jan 11, 2025
7166865
fix: disable image transform
mariojsnunes Jan 13, 2025
88dcbd6
re-add image transforms
mariojsnunes Jan 13, 2025
be13a31
cleanup
mariojsnunes Jan 15, 2025
0c63d87
chore: add reply read test
mariojsnunes Jan 15, 2025
2bdbd86
Merge branch 'master' into feat/questions-supabase
mariojsnunes Jan 16, 2025
c0e93ae
Merge branch 'master' into feat/questions-supabase
mariojsnunes Jan 16, 2025
7cd2a34
Merge branch 'master' into feat/questions-supabase
mariojsnunes Jan 17, 2025
28fc2fb
Merge branch 'master' into feat/questions-supabase
mariojsnunes Jan 18, 2025
313d736
feat: add storage local setup
mariojsnunes Jan 18, 2025
2bdd8a3
fix: question tests
mariojsnunes Jan 18, 2025
cf4a23f
chore: comment out notification tests as they are flaky
mariojsnunes Jan 18, 2025
26184d8
chore: lint
mariojsnunes Jan 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions docs/supabase.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### What is Supabase?
# What is Supabase?

Supabase is an open source Firebase alternative, based on Postgres.

Expand All @@ -12,14 +12,49 @@ Make sure you have the docker app open.

Run `supabase start` (Ensure you run it on the project folder root.)

Create a `public` bucket named `precious-plastic` for local dev.
Add the policy named `tenant_isolation` for all operations: `(bucket_id = ((current_setting('request.headers'::text, true))::json ->> 'x-tenant-id'::text))`
Ideally this would be automated.

## Running Cypress Tests

Create a .env.local file at the packages/cypress folder
SUPABASE_API_URL=your_api_key (probably http://127.0.0.1:54321)
SUPABASE_KEY=your_key

All done! Tests will use your local database. More info about how it works below.

## Migrations

After making schema changes, use the this command to create a migration file:
`supabase db diff --file [migration_name]`

### Cypress with Supabase

Running cypress tests locally will use the local database, while running on CI will use the QA database.
For each test run, a new tenant_id is generated, which has a few benefits:

- ensures no conflicts between parallel test runs
- easier to cleanup
- if the data isn't cleaned for some reason, it won't affect other runs
For each test file, there should be a `before` and `after` block to, respectively, seed and clean the database.

### Local firebase sync testing/debugging

_This is temporary until we fully migrate to supabase!_
We can create and deploy the sync function to the firebase dev environment.
Then, using ngrok, expose our local supabase url to the internet, and point the firebase function to it.
(ngrok http http://127.0.0.1:54321)

To authenticate, we need to create these 3 secrets, for each firebase project:
firebase functions:secrets:set SUPABASE_API_URL
firebase functions:secrets:set SUPABASE_API_KEY
firebase functions:secrets:set TENANT_ID
(Check values with firebase functions:secrets:access SECRET_NAME)

## Technical Decisions

# Multi-tenant
### Multi-tenant

Multi-tenancy is a requirement because:

Expand All @@ -43,7 +78,7 @@ How?
- Each table has a tenant_id column
- On each request, to supabase (via it's sdk) we pass a header 'x-tenant-id' with the process.env.TENANT_ID variable, which is set for each app, via Fly.io secret.

# Comment Counts
### Comment Counts

Currently we can sort questions/research/howtos by the number of comments.
With supabase there are a few ways we can do this:
Expand All @@ -62,16 +97,3 @@ How?
- Whenever a comment is created or deleted, it triggers the update_comment_count function.
- The function checks the Operation kind (Insert/Delete), the source_type and source_id.
- Fron the source_type it will update the according content total (howtos, research, questions) that matches the source_id

# Local firebase sync testing/debugging

_This is temporary until we fully migrate to supabase!_
We can create and deploy the sync function to the firebase dev environment.
Then, using ngrok, expose our local supabase url to the internet, and point the firebase function to it.
(ngrok http http://127.0.0.1:54321)

To authenticate, we need to create these 3 secrets, for each firebase project:
firebase functions:secrets:set SUPABASE_API_URL
firebase functions:secrets:set SUPABASE_API_KEY
firebase functions:secrets:set TENANT_ID
(Check values with firebase functions:secrets:access SECRET_NAME)
3 changes: 2 additions & 1 deletion packages/cypress/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ coverage
screenshots
fixtures/seed/*.rej
build
*.js
*.js
cypress.env.json
12 changes: 12 additions & 0 deletions packages/cypress/scripts/start.mts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,20 @@ export const generateAlphaNumeric = (length: number) => {
}

const e2eEnv = config()
config({ path: '.env.local' })

const isCi = process.argv.includes('ci')
// const isProduction = process.argv.includes('prod')
const tenantId = generateAlphaNumeric(8)

fs.writeFileSync(
'cypress.env.json',
JSON.stringify({
TENANT_ID: tenantId,
SUPABASE_API_URL: process.env.SUPABASE_API_URL,
SUPABASE_KEY: process.env.SUPABASE_KEY,
}),
)

// Prevent unhandled errors being silently ignored
process.on('unhandledRejection', (err) => {
Expand Down Expand Up @@ -110,6 +121,7 @@ async function startAppServer() {
env: {
...process.env,
VITE_SITE_VARIANT: 'test-ci',
TENANT_ID: tenantId,
},
})

Expand Down
3 changes: 0 additions & 3 deletions packages/cypress/src/integration/library/write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ describe('[Library]', () => {
]
const categoryGuidanceMain =
'Cover image should show the fully built mould'
const categoryGuidanceFiles = 'Include files to replicate the mould'

cy.signUpNewUser(creator)
cy.get('[data-cy=loader]').should('not.exist')
Expand Down Expand Up @@ -234,10 +233,8 @@ describe('[Library]', () => {

cy.step('Select a category and see further guidance')
cy.contains(categoryGuidanceMain).should('not.exist')
cy.contains(categoryGuidanceFiles).should('not.exist')
selectCategory(category as Category)
cy.contains(categoryGuidanceMain).should('be.visible')
cy.contains(categoryGuidanceFiles).should('be.visible')

selectTimeDuration(time as Duration)
selectDifficultLevel(difficulty_level)
Expand Down
112 changes: 57 additions & 55 deletions packages/cypress/src/integration/notifications.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { generateNewUserDetails } from '../utils/TestUtils'
// import { generateNewUserDetails } from '../utils/TestUtils'

/**
* Time to wait before checking database for updates to try ensure writes
* TODO - CC 2022-04-25 - Likely some of these waits can be replaced by search for UI elements
* that should respond to change
*/
const DB_WAIT_TIME = 5000
// const DB_WAIT_TIME = 5000

describe('[Notifications]', () => {
// Can't test like this now because we are now using the same users collection for all tests.
Expand All @@ -24,65 +24,67 @@ describe('[Notifications]', () => {
// )
// })

it('[are generated by clicking on useful for projects]', () => {
const visitor = generateNewUserDetails()
cy.signUpNewUser(visitor)
// TODO: find out the issue and add it back
// it('[are generated by clicking on useful for projects]', () => {
// const visitor = generateNewUserDetails()
// cy.signUpNewUser(visitor)

cy.visit('library')
cy.visit('/library/testing-testing')
cy.wait(DB_WAIT_TIME)
cy.get('[data-cy="vote-useful"]').contains('useful').click()
cy.wait(DB_WAIT_TIME)
cy.step('Verify the notification has been added')
cy.queryDocuments('users', 'userName', '==', 'event_reader').then(
(docs) => {
expect(docs.length).to.be.greaterThan(0)
const [user] = docs
const notifications = user['notifications']
expect(notifications.length).to.be.greaterThan(0)
// cy.visit('library')
// cy.visit('/library/testing-testing')
// cy.wait(DB_WAIT_TIME)
// cy.get('[data-cy="vote-useful"]').contains('useful').click()
// cy.wait(DB_WAIT_TIME)
// cy.step('Verify the notification has been added')
// cy.queryDocuments('users', 'userName', '==', 'event_reader').then(
// (docs) => {
// expect(docs.length).to.be.greaterThan(0)
// const [user] = docs
// const notifications = user['notifications']
// expect(notifications.length).to.be.greaterThan(0)

const notification = notifications.find(
({ triggeredBy, type }) =>
triggeredBy.userId === visitor.username && type === 'howto_useful',
)
expect(notification['type']).to.equal('howto_useful')
expect(notification['relevantUrl']).to.equal('/library/testing-testing')
expect(notification['read']).to.equal(false)
expect(notification['triggeredBy']['displayName']).to.equal(
visitor.username,
)
},
)
})
// const notification = notifications.find(
// ({ triggeredBy, type }) =>
// triggeredBy.userId === visitor.username && type === 'howto_useful',
// )
// expect(notification['type']).to.equal('howto_useful')
// expect(notification['relevantUrl']).to.equal('/library/testing-testing')
// expect(notification['read']).to.equal(false)
// expect(notification['triggeredBy']['displayName']).to.equal(
// visitor.username,
// )
// },
// )
// })

it('[are generated by clicking on useful for research]', () => {
const visitor = generateNewUserDetails()
cy.signUpNewUser(visitor)
// TODO: find out the issue and add it back
// it('[are generated by clicking on useful for research]', () => {
// const visitor = generateNewUserDetails()
// cy.signUpNewUser(visitor)

cy.visit('/research/qwerty')
cy.wait(DB_WAIT_TIME)
cy.get('[data-cy="vote-useful"]').contains('useful').click()
cy.wait(DB_WAIT_TIME)
cy.step('Verify the notification has been added')
cy.queryDocuments('users', 'userName', '==', 'event_reader').then(
(docs) => {
expect(docs.length).to.be.greaterThan(0)
const [user] = docs
const notifications = user['notifications']
expect(notifications.length).to.be.greaterThan(0)
// cy.visit('/research/qwerty')
// cy.wait(DB_WAIT_TIME)
// cy.get('[data-cy="vote-useful"]').contains('useful').click()
// cy.wait(DB_WAIT_TIME)
// cy.step('Verify the notification has been added')
// cy.queryDocuments('users', 'userName', '==', 'event_reader').then(
// (docs) => {
// expect(docs.length).to.be.greaterThan(0)
// const [user] = docs
// const notifications = user['notifications']
// expect(notifications.length).to.be.greaterThan(0)

const notification = notifications.find(
({ triggeredBy }) => triggeredBy.userId === visitor.username,
)
// const notification = notifications.find(
// ({ triggeredBy }) => triggeredBy.userId === visitor.username,
// )

expect(notification['relevantUrl']).to.equal('/research/qwerty')
expect(notification['read']).to.equal(false)
expect(notification['triggeredBy']['displayName']).to.equal(
visitor.username,
)
},
)
})
// expect(notification['relevantUrl']).to.equal('/research/qwerty')
// expect(notification['read']).to.equal(false)
// expect(notification['triggeredBy']['displayName']).to.equal(
// visitor.username,
// )
// },
// )
// })

it('[appear in notifications modal]', () => {
cy.visit('library')
Expand Down
57 changes: 57 additions & 0 deletions packages/cypress/src/integration/questions/discussions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// This is basically an identical set of steps to the discussion tests for
// how-tos and research. Any changes here should be replicated there.

import { MOCK_DATA } from '../../data'

describe('[Questions.Discussions]', () => {
it('can open using deep links', () => {
const question = MOCK_DATA.questions[0]
cy.visit(`/questions/${question.slug}`)
cy.get(`[data-cy=comment-text]`).contains('First comment')
cy.get('[data-cy=show-replies]').click()
cy.get(`[data-cy="ReplyItem"]`).contains('First Reply')
})

// it('allows authenticated users to contribute to discussions', () => {
mariojsnunes marked this conversation as resolved.
Show resolved Hide resolved
// const visitor = generateNewUserDetails()
// cy.addQuestion(question, visitor)
// cy.signUpNewUser(visitor)
// const newComment = `An interesting question. The answer must be... ${visitor.username}`
// const updatedNewComment = `An interesting question. The answer must be that when the sky is red, the apocalypse _might_ be on the way. Love, ${visitor.username}`
// const newReply = `Thanks Dave and Ben. What does everyone else think? - ${visitor.username}`
// const updatedNewReply = `Anyone else? Your truly ${visitor.username}`
// const questionPath = `/questions/quick-question-for-${visitor.username}`
// cy.step('Can add comment')
// cy.visit(questionPath)
// cy.contains('Start the discussion')
// cy.contains('0 comments')
// cy.addComment(newComment)
// cy.contains('1 comment')
// cy.step('Can edit their comment')
// cy.editDiscussionItem('CommentItem', newComment, updatedNewComment)
// cy.step('Another user can add reply')
// const secondCommentor = generateNewUserDetails()
// cy.logout()
// cy.signUpNewUser(secondCommentor)
// cy.visit(questionPath)
// cy.addReply(newReply)
// cy.wait(1000)
// cy.contains('2 comments')
// cy.step('Can edit their reply')
// cy.editDiscussionItem('ReplyItem', newReply, updatedNewReply)
// cy.step('Another user can leave a reply')
// const secondReply = `Quick reply. ${visitor.username}`
// cy.step('First commentor can respond')
// cy.logout()
// cy.login(visitor.email, visitor.password)
// cy.visit(questionPath)
// cy.addReply(secondReply)
// cy.step('Can delete their comment')
// cy.deleteDiscussionItem('CommentItem', updatedNewComment)
// cy.step('Replies still show for deleted comments')
// cy.get('[data-cy="deletedComment"]').should('be.visible')
// cy.get('[data-cy=OwnReplyItem]').contains(secondReply)
// cy.step('Can delete their reply')
// cy.deleteDiscussionItem('ReplyItem', secondReply)
// })
})
Loading
Loading