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

Structure Tools #407

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4e9b177
shebang should be first line
CocoisBuggy Nov 2, 2024
8b66de3
For those using bash beautify vscode extension
CocoisBuggy Nov 2, 2024
a9c8527
Make replica set specification optional in seed script
CocoisBuggy Nov 2, 2024
0b0d630
Add a shell.nix for people using nixOS
CocoisBuggy Nov 2, 2024
c0eb188
Integrate memory server into nixos environment
CocoisBuggy Nov 2, 2024
bf1f307
compass is a neat tool - though duplicates much of the functionality …
CocoisBuggy Nov 3, 2024
6ca34b2
A naieve implementation of the functionality I was thinking of using.
CocoisBuggy Nov 3, 2024
027b0e6
I think I may have been wrong about this. Path Hash is not a cached
CocoisBuggy Nov 3, 2024
4794107
Performance improvements and less jank
CocoisBuggy Nov 4, 2024
9c58cfe
minor speed improvement to the query
CocoisBuggy Nov 4, 2024
c72f95e
minor speed improvement to the query
CocoisBuggy Nov 4, 2024
198d906
Tests added and param options implemented
CocoisBuggy Nov 4, 2024
671e3b4
Merge branch 'coco-hierarchy-tools' of github.com:OpenBeta/openbeta-g…
CocoisBuggy Nov 4, 2024
9551722
Add utility for using or creating a client session
CocoisBuggy Nov 7, 2024
2790855
Merge branch 'coco/#409' into coco/area-relational-integrity
CocoisBuggy Nov 8, 2024
dfac7d1
update script to allow 'mongosh'
CocoisBuggy Nov 8, 2024
53e0b80
Migrate to new data model
CocoisBuggy Nov 8, 2024
e0c1545
Allow compass to open on the correct connection
CocoisBuggy Nov 9, 2024
26d22c4
Begin integreating the parent-centric changes
CocoisBuggy Nov 10, 2024
ff27ec2
Add debug flag to tests
CocoisBuggy Nov 12, 2024
6e6f251
Add an attach request to the launch options
CocoisBuggy Nov 12, 2024
04e7430
Prefer optional dbg over forced
CocoisBuggy Nov 12, 2024
af694b2
I believe we have moved passed the need for import
CocoisBuggy Nov 12, 2024
2e9c378
Denormalize related data together
CocoisBuggy Nov 12, 2024
bf7da84
Merge branch 'develop' into coco/area-relational-integrity
CocoisBuggy Nov 12, 2024
431f285
Area ancestry deconvolution
CocoisBuggy Nov 14, 2024
2363740
Checkpoint commit -- pls squash
CocoisBuggy Nov 18, 2024
99e77d8
Checkpoint - back from mountains
CocoisBuggy Nov 25, 2024
ffd4363
Area parent migration implemented and tested
CocoisBuggy Nov 28, 2024
79511ee
Expose functionality via the gql layer
CocoisBuggy Nov 28, 2024
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
8 changes: 7 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"history"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
"internalConsoleOptions": "neverOpen",
},
{
"type": "node",
Expand All @@ -110,6 +110,12 @@
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Jest (Yarn)",
"port": 9229,
}
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"javascript.format.semicolons": "remove",
"typescript.format.enable": false,
"prettier.enable": false,
"editor.defaultFormatter": "standard.vscode-standard"
"editor.defaultFormatter": "standard.vscode-standard",
"bashBeautify.tabSize": 2
}
100 changes: 100 additions & 0 deletions db-migrations/0006-area-structure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// For each document that has children, we want to tell its children to back-link to us.
db.areas
.find({ children: { $exists: true, $type: "array" } })
.forEach((parentDoc) =>
db.areas.updateMany(
{ _id: { $in: parentDoc.children } },
{ $set: { parent: parentDoc._id } },
),
);

// Pre-fetch children for all documents to avoid querying in the loop
const allChildren = db.areas.aggregate([
{ $match: { parent: { $exists: true } } },
{ $group: { _id: "$parent", children: { $push: "$_id" } } }
]).toArray();

// hold a reference to the children in memory
const childrenMap = allChildren.reduce((map, item) => {
map[item._id] = item.children;
return map;
}, {});

// This stage will take a WHILE
db.areas.find().forEach((doc) => {
// Perform a $graphLookup aggregation to get the full ancestor path for our target
const pathDocs = db.areas.aggregate([
{
$match: { _id: doc._id },
},
{
$graphLookup: {
from: "areas",
startWith: "$parent",
connectFromField: "parent",
connectToField: "_id",
as: "ancestorPath",
depthField: "depth",
},
},
{
$unwind: "$ancestorPath",
},
{
$sort: { "ancestorPath.depth": -1 },
},
{
$group: {
_id: "$_id",
ancestors: { $push: '$ancestorPath' }
},
},
]).toArray();

const embeddedRelations = {
children: childrenMap[doc._id] || [],
// map out the ancestors of this doc (terminating at the current node for backward-compat reasons)
// We take out the relevant data we would like to be denormalized.
ancestors: [...(pathDocs[0]?.ancestors ?? []), doc].map(i => ({
_id: i._id,
name: i.area_name,
uuid: i.metadata.area_id
}))
};

if (embeddedRelations.ancestors.map(i => i.name).join(",") !== doc.pathTokens.join(",")) {
throw `Path tokens did not match (${embeddedRelations.ancestors.map(i => i.name)} != ${doc.pathTokens})`;
}

if (embeddedRelations.ancestors.map(i => i.uuid).join(',') !== doc.ancestors) {
throw `Ancestors did not match (${embeddedRelations.ancestors.map(i => i.uuid)} != ${doc.ancestors})`;
}


// Use bulkWrite for efficient updates
db.areas.updateOne(
{ _id: doc._id },
{ $set: { embeddedRelations } }
);
});

print("Removing old fields.");

// Remove the unneeded values since all ops have run without raising an assertion issue
db.areas.updateMany({}, {
$unset: { children: "", pathTokens: "", ancestors: "" },
});

printjson(db.areas.createIndex({ parent: 1 }))
printjson(db.areas.createIndex({ 'embeddedRelations.children': 1 }))
printjson(db.areas.createIndex({ 'embeddedRelations.ancestors._id': 1 }))
printjson(db.areas.createIndex({ 'embeddedRelations.ancestors.uuid': 1 }))
printjson(db.areas.createIndex({ 'embeddedRelations.ancestors.name': 1 }))

printjson(db.areas.createIndex({ 'embeddedRelations.ancestors._id': 1, 'embeddedRelations.ancestors.name': 1 }))


// https://www.mongodb.com/docs/v6.2/reference/method/db.collection.createIndex/#create-an-index-on-a-multiple-fields
// > The order of fields in a compound index is important for supporting sort() operations using the index.
// It is not clear to me if there is a $lookup speed implication based on the direction of the join.
printjson(db.areas.createIndex({ parent: 1, _id: 1 }))
19 changes: 18 additions & 1 deletion migrate-db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@ then
exit 1
fi

if [ ! -f $1 ]
then
echo "Specified migration file ($1) not found"
exit 1
fi

. .env

connStr="${MONGO_SCHEME}://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@${MONGO_SERVICE}/${MONGO_DBNAME}?authSource=${MONGO_AUTHDB}&tls=${MONGO_TLS}&replicaSet=${MONGO_REPLICA_SET_NAME}"

mongo "$connStr" $1
# Determine whether `mongo` or `mongosh` is available
if command -v mongosh &> /dev/null; then
mongoCmd="mongosh"
elif command -v mongo &> /dev/null; then
mongoCmd="mongo"
else
echo "Neither mongosh nor mongo command found. Please install one of them."
exit 1
fi

# Execute the chosen command with the connection string and migration file
"$mongoCmd" "$connStr" "$1"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"lint": "yarn ts-standard",
"fix": "yarn ts-standard --fix",
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --runInBand",
"test:dbg": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --inspect-brk\" jest --runInBand",
"build": "tsc -p tsconfig.json",
"build-release": "tsc -p tsconfig.release.json",
"clean": "tsc -b --clean && rm -rf build/*",
Expand Down
10 changes: 7 additions & 3 deletions seed-db.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash
# Rebuild your local database with a copy of OpenBeta staging database.
#
# To keep running time short, the script only downloads the remote
# To keep running time short, the script only downloads the remote
# database dump file once. Specify 'download' argument to force download.
#
# Syntax:
# ./seed-db.sh [download]
#
#!/bin/bash

FILE_NAME="openbeta-stg-db.tar.gz"
REMOTE_FILE="https://storage.googleapis.com/openbeta-dev-dbs/$FILE_NAME"
Expand All @@ -22,7 +22,11 @@ tar xzf $FILE_NAME

. .env

connStr="${MONGO_SCHEME}://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@${MONGO_SERVICE}/${MONGO_DBNAME}?authSource=${MONGO_AUTHDB}&tls=${MONGO_TLS}&replicaSet=${MONGO_REPLICA_SET_NAME}"
connStr="${MONGO_SCHEME}://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@${MONGO_SERVICE}/${MONGO_DBNAME}?authSource=${MONGO_AUTHDB}&tls=${MONGO_TLS}"

if [ -z "${MONGO_REPLICA_SET_NAME}" ]; then
connStr+="&replicaSet=${MONGO_REPLICA_SET_NAME}"
fi

mongorestore --uri "$connStr" -d=${MONGO_DBNAME} --gzip --drop ./db-dumps/staging/openbeta

44 changes: 44 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
pkgs ? import <nixpkgs> {
config = {
allowUnfree = true;
};
},
}:
pkgs.mkShell {
buildInputs = with pkgs; [
mongodb-tools
yarn
mongodb-ce
mongodb-compass
mongosh
gsettings-desktop-schemas

deno
];

# MONGOMS_DOWNLOAD_URL = "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2404-8.0.1.tgz";
MONGOMS_DISTRO = "ubuntu-22.04";
MONGOMS_RUNTIME_DOWNLOAD = false;
MONGOMS_SYSTEM_BINARY = "${pkgs.mongodb-ce}/bin/mongod";
# you will need to keep this value in sync with the pre-built mongodb-ce
# (or you can use the mongodb package which will build from source and take a WHILE)
# https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/by-name/mo/mongodb-ce/package.nix#L113
MONGOMS_VERSION = "7.0.14";

shellHook = ''

set -a
source .env
mongo_cnx="$MONGO_SCHEME://$MONGO_INITDB_ROOT_USERNAME:$MONGO_INITDB_ROOT_PASSWORD@$MONGO_SERVICE/$MONGO_DBNAME?authSource=$MONGO_AUTHDB&tls=$MONGO_TLS"

# mongotop alias
alias mto="mongotop --uri=$mongo_cnx"
# mongostat alias
alias mst="mongostat --uri=$mongo_cnx"
# Compass tooling
alias compass="mongodb-compass --theme=dark $mongo_cnx"

echo "🧗 Alle!"
'';
}
87 changes: 86 additions & 1 deletion src/__tests__/areas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApolloServer } from 'apollo-server-express'
import muuid from 'uuid-mongodb'
import muuid, { MUUID } from 'uuid-mongodb'
import { jest } from '@jest/globals'
import MutableAreaDataSource from '../model/MutableAreaDataSource.js'
import MutableOrganizationDataSource from '../model/MutableOrganizationDataSource.js'
Expand Down Expand Up @@ -107,4 +107,89 @@ describe('areas API', () => {
expect(areaResult.organizations[0].orgId).toBe(muuidToString(alphaOrg.orgId))
})
})

describe('area structure API', () => {
const structureQuery = `
query structure($parent: ID!) {
structure(parent: $parent) {
parent
uuid
area_name
climbs
}
}
`

// Structure queries do not do write operations so we can build this once
beforeEach(async () => {
const maxDepth = 4
const maxBreadth = 3

// So for the purposes of this test we will do a simple tree
async function grow (from: MUUID, depth: number = 0): Promise<void> {
if (depth >= maxDepth) return
for (const idx of Array.from({ length: maxBreadth }, (_, i) => i + 1)) {
const newArea = await areas.addArea(user, `${depth}-${idx}`, from)
await grow(newArea.metadata.area_id, depth + 1)
}
}

await grow(usa.metadata.area_id)
})

it('retrieves the structure of a given area', async () => {
const response = await queryAPI({
query: structureQuery,
operationName: 'structure',
variables: { parent: usa.metadata.area_id },
userUuid,
app
})

expect(response.statusCode).toBe(200)
})

it('should allow no parent to be supplied and get a shallow result', async () => {
const response = await queryAPI({
query: `
query structure {
structure {
parent
uuid
area_name
climbs
}
}
`,
operationName: 'structure',
userUuid,
app
})

expect(response.statusCode).toBe(200)
})

it('should allow calling of the setAreaParent gql endpoint.', async () => {
const testArea = await areas.addArea(muuid.from(userUuid), 'A Rolling Stone', usa.metadata.area_id)

const response = await queryAPI({
query: `
mutation SetAreaParent($area: ID!, $newParent: ID!) {
setAreaParent(area: $area, newParent: $newParent) {
areaName
area_name
}
}
`,
operationName: 'SetAreaParent',
userUuid,
app,
// Move it to canada
variables: { area: testArea.metadata.area_id, newParent: ca.metadata.area_id }
})

console.log(response.body)
expect(response.statusCode).toBe(200)
})
})
})
13 changes: 13 additions & 0 deletions src/auth/local-dev/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import muuid, { MUUID } from 'uuid-mongodb'
import { AuthUserType } from '../../types.js'
import { logger } from '../../logger.js'
import type { ApolloServerPlugin } from 'apollo-server-plugin-base'

export const localDevBypassAuthContext = (() => {
const testUUID: MUUID = muuid.v4()
Expand All @@ -19,3 +20,15 @@ export const localDevBypassAuthContext = (() => {
return { user }
}
})()

export const loggerPlugin: ApolloServerPlugin = {
async requestDidStart (ctx) {
const startTime = process.uptime()
return {
async willSendResponse () {
ctx.logger.info(ctx.request.query)
ctx.logger.info(`query resolved in ${((process.uptime() - startTime) * 1000).toFixed(4)}ms`)
}
}
}
}
Loading
Loading