diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 924d8afa..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "statusLine": { - "type": "command", - "command": "bash /mnt/e/rerum_server_nodejs/.claude/statusline-command.sh" - }, - "env": { - "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "500000", - "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1", - "MAX_MCP_OUTPUT_TOKENS": "500000", - "DISABLE_ERROR_REPORTING": "0", - "DISABLE_NON_ESSENTIAL_MODEL_CALLS": "0", - "DISABLE_PROMPT_CACHING": "0", - "MAX_THINKING_TOKENS": "500000", - "BASH_MAX_TIMEOUT_MS": "3000000", - "OPENCODE_DISABLE_PRUNE": "true", - "OPENCODE_DISABLE_AUTOCOMPACT": "true" - } -} diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d3d5a899..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(env)", - "Bash(npm install:*)", - "Bash(npm run:*)", - "Bash(npm start:*)", - "Bash(pm2:*)", - "Bash(git:*)", - "Bash(node:*)", - "Bash(curl:*)", - "Bash(mongosh:*)", - "Read(//tmp/**)", - "Bash(bash:*)", - "Bash(tee:*)", - "Bash(echo:*)", - "Bash(cat:*)", - "Bash(python3:*)", - "WebSearch", - "WebFetch(domain:github.com)", - "WebFetch(domain:raw.githubusercontent.com)" - ], - "deny": [], - "ask": [] - } -} diff --git a/.claude/statusline-command.sh b/.claude/statusline-command.sh deleted file mode 100644 index ed13514a..00000000 --- a/.claude/statusline-command.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# Read JSON input -input=$(cat) - -# Extract data from JSON -cwd=$(echo "$input" | jq -r '.workspace.current_dir') -cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0') -api_duration=$(echo "$input" | jq -r '.cost.total_api_duration_ms // 0') -total_duration=$(echo "$input" | jq -r '.cost.total_duration_ms // 0') -lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0') -lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0') -model_display=$(echo "$input" | jq -r '.model.display_name // "unknown"') - -# Calculate API duration in seconds -api_duration_sec=$(echo "scale=1; $api_duration / 1000" | bc -l 2>/dev/null || echo "0") - -# Get git branch if in a git repository -git_branch="" -if git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then - branch=$(git -C "$cwd" -c core.fileMode=false branch --show-current 2>/dev/null) - if [ -n "$branch" ]; then - git_branch="($branch)" - fi -fi - -# Build the enhanced status line -# Format: (branch) model $cost | API: Xs | +L/-L - -# Cyan for git branch -if [ -n "$git_branch" ]; then - printf '\033[36m%s\033[0m ' "$git_branch" -fi - -# Magenta for model name -printf '\033[35m%s\033[0m ' "$model_display" - -# Bold yellow for cost (live updating token usage proxy) -printf '\033[1;33m$%.4f\033[0m' "$cost" - -# Green for API time (shows compute usage) -if [ "$api_duration" != "0" ]; then - printf ' \033[32m| API: %ss\033[0m' "$api_duration_sec" -fi - -# White for code changes (productivity) -if [ "$lines_added" != "0" ] || [ "$lines_removed" != "0" ]; then - printf ' \033[37m| +%s/-%s\033[0m' "$lines_added" "$lines_removed" -fi - -printf '\n' diff --git a/.github/workflows/cd_dev.yaml b/.github/workflows/cd_dev.yaml deleted file mode 100644 index 0f6dbca2..00000000 --- a/.github/workflows/cd_dev.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: RERUM Server v1 Development Deploy on PR to main. -on: - pull_request: - branches: main -jobs: - merge-branch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Merge with main - uses: devmasx/merge-branch@master - with: - type: now - from_branch: main - target_branch: ${{ github.head_ref }} - github_token: ${{ secrets.BRY_PAT }} - message: Merge main into this branch to deploy to dev for testing. - test: - needs: merge-branch - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Create .env from secrets - run: echo "${{ secrets.DEV_FULL_ENV }}" > .env - - name: Setup Node.js - uses: actions/setup-node@master - with: - node-version: "24" - - name: Cache node modules - uses: actions/cache@master - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ - hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install dependencies and run the test - run: | - npm install - npm run runtest - deploy: - if: github.event.pull_request.draft == false - needs: - - merge-branch - - test - strategy: - matrix: - node-version: - - 24 - machines: - - vlcdhp02 - runs-on: ${{ matrix.machines }} - steps: - - uses: actions/checkout@master - - name: Deploy the app on the server - run: | - if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then - mkdir -p /srv/node/logs - touch /srv/node/logs/rerumv1.txt - fi - cd /srv/node/v1-node/ - pm2 stop rerum_v1 - git stash - git pull - git checkout ${{ github.head_ref }} - git stash - git pull - npm install - pm2 start -i max bin/rerum_v1.js diff --git a/.github/workflows/cd_prod.yaml b/.github/workflows/cd_prod.yaml deleted file mode 100644 index 70ea945a..00000000 --- a/.github/workflows/cd_prod.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: RERUM Server v1 Production Deploy on push to main. -on: - push: - branches: main -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Create .env from secrets - run: echo "${{ secrets.PROD_FULL_ENV }}" > .env - - name: Setup Node.js - uses: actions/setup-node@master - with: - node-version: "24" - - # Speed up subsequent runs with caching - - name: Cache node modules - uses: actions/cache@master - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ - hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install dependencies and run the test - run: | - npm install - npm run runtest - deploy: - needs: test - strategy: - matrix: - node-version: - - 24 - machines: - - vlcdhprdp02 - runs-on: ${{ matrix.machines }} - steps: - - uses: actions/checkout@master - - name: Deploy the app on the server - run: | - if [[ ! -e /srv/node/logs/rerumv1.txt ]]; then - mkdir -p /srv/node/logs - touch /srv/node/logs/rerumv1.txt - fi - cd /srv/node/v1-node/ - pm2 stop rerum_v1 - git stash - git checkout main - git stash - git pull - npm install - pm2 start -i max bin/rerum_v1.js diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml deleted file mode 100644 index 596a39a9..00000000 --- a/.github/workflows/claude.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Claude Code -on: - issues: - types: [opened] - issue_comment: - types: [created] - pull_request_review: - types: [submitted] - pull_request_review_comment: - types: [created] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - issues: write - id-token: write - actions: read - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - show_full_output: true - # trigger_phrase: "claude do the needful" \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..0f36221d --- /dev/null +++ b/.npmignore @@ -0,0 +1,30 @@ +# Documentation +docs/ +*.md +!README.md +CODEOWNERS + +# Test and mock files +__tests__/ +__mocks__/ +**/__tests__/ +**/__mocks__/ +coverage/ +jest.config.js + +# Backup folders / artifacts +backups/ +*.backup +*.bak +*.swp +*.tgz + +# Local configuration files +.env +.env.* +config.local.js +*.local.js + +# Tooling and CI +.github/ +.claude/ \ No newline at end of file diff --git a/README.md b/README.md index ae6c6917..04a7b8fe 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,27 @@ Stores important bits of knowledge in structured JSON-LD objects: 1. **Open and Free**—expose all contributions immediately without charge to write or read; 1. **Attributed and Versioned**—always include asserted ownership and transaction metadata so consumers can evaluate trustworthiness and relevance. +### Programmatic usage +This project exposes a single public entry point at the package root (`index.js`). Only a few +functions are exported – everything else lives in internal modules and is intentionally +kept private. Example: + +```js +import { app, createServer, start } from 'rerum_server' + +// `app` is the configured Express application; you can pass it to Supertest or reuse it +// inside another HTTP stack. + +const server = createServer(8080) // returns a http.Server but does not listen +server.listen() + +// or simply +start(8080) // convenience helper that both creates and listens +``` + +Consumers no longer need to reach into `./app.js` or other deep paths – if it isn't +exported here it isn't part of the stable API. + ## What we add You will find a `__rerum` property on anything you read from this repository. This is written onto all objects by the server and is not editable by the client applications. While applications may assert diff --git a/__tests__/public_api.test.js b/__tests__/public_api.test.js new file mode 100644 index 00000000..12ac4eb5 --- /dev/null +++ b/__tests__/public_api.test.js @@ -0,0 +1,22 @@ +import { app, createServer, start } from '../index.js' + +describe('public API entry point', () => { + test('exports an express app instance', () => { + expect(app).toBeDefined() + expect(typeof app.use).toBe('function') // express app + }) + + test('createServer returns a http.Server', () => { + const server = createServer(0) // port 0 for ephemeral + expect(server).toBeDefined() + expect(typeof server.listen).toBe('function') + server.close() + }) + + test('start starts the server and returns it', (done) => { + const server = start(0) + server.on('listening', () => { + server.close(() => done()) + }) + }) +}) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index edd53716..bbddc2d9 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -7,7 +7,8 @@ import request from "supertest" import api_routes from "../routes/api-routes.js" -import app from "../app.js" +// leverage the public entry point instead of a deep path +import app from "../index.js" import fs from "fs" let app_stack = app.router.stack diff --git a/app.js b/app.js index fa6e7900..548f9092 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,7 @@ import express from 'express' import path from 'path' import cookieParser from 'cookie-parser' -import dotenv from 'dotenv' -dotenv.config() +import config from './config/index.js' import logger from 'morgan' import cors from 'cors' import indexRouter from './routes/index.js' @@ -71,7 +70,7 @@ app.use(express.static(path.join(__dirname, 'public'))) * This is without middleware */ app.all('*_', (req, res, next) => { - if(process.env.DOWN === "true"){ + if(config.DOWN === "true"){ res.status(503).json({"message":"RERUM v1 is down for updates or maintenance at this time. We apologize for the inconvenience. Try again later."}) } else{ diff --git a/auth/index.js b/auth/index.js index 695fe9b4..9c9aaaba 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,6 +1,5 @@ import { auth } from 'express-oauth2-jwt-bearer' -import dotenv from 'dotenv' -dotenv.config() +import config from '../config/index.js' const _tokenError = function (err, req, res, next) { if(!err.code || err.code !== "invalid_token"){ @@ -55,10 +54,10 @@ const generateNewAccessToken = async (req, res, next) => { console.log("RERUM v1 is generating a proxy access token.") const form = { grant_type: 'refresh_token', - client_id: process.env.CLIENT_ID, - client_secret: process.env.CLIENT_SECRET, + client_id: config.CLIENT_ID, + client_secret: config.CLIENT_SECRET, refresh_token: req.body.refresh_token, - redirect_uri:process.env.RERUM_PREFIX + redirect_uri: config.RERUM_PREFIX } try{ // Successful responses from auth 0 look like {"refresh_token":"BLAHBLAH", "access_token":"BLAHBLAH"} @@ -101,10 +100,10 @@ const generateNewRefreshToken = async (req, res, next) => { console.log("RERUM v1 is generating a new refresh token.") const form = { grant_type: 'authorization_code', - client_id: process.env.CLIENT_ID, - client_secret: process.env.CLIENT_SECRET, + client_id: config.CLIENT_ID, + client_secret: config.CLIENT_SECRET, code: req.body.authorization_code, - redirect_uri:process.env.RERUM_PREFIX + redirect_uri: config.RERUM_PREFIX } try { // Successful responses from auth 0 look like {"refresh_token":"BLAHBLAH", "access_token":"BLAHBLAH"} @@ -160,7 +159,7 @@ const verifyAccess = (secret) => { * @returns Boolean match between encoded Generator Agent and obj generator */ const isGenerator = (obj, userObj) => { - return userObj[process.env.RERUM_AGENT_CLAIM] === obj.__rerum.generatedBy + return userObj[config.RERUM_AGENT_CLAIM] === obj.__rerum.generatedBy } /** @@ -170,11 +169,11 @@ const isGenerator = (obj, userObj) => { * @returns Boolean for matching ID. */ const isBot = (userObj) => { - return process.env.BOT_AGENT === userObj[process.env.RERUM_AGENT_CLAIM] + return config.BOT_AGENT === userObj[config.RERUM_AGENT_CLAIM] } function READONLY(req, res, next) { - if(process.env.READONLY=="true"){ + if(config.READONLY=="true"){ res.status(503).json({"message":"RERUM v1 is read only at this time. We apologize for the inconvenience. Try again later."}) return } diff --git a/bin/rerum_v1.js b/bin/rerum_v1.js index 8b269269..a1b953d6 100644 --- a/bin/rerum_v1.js +++ b/bin/rerum_v1.js @@ -8,14 +8,13 @@ import app from '../app.js' import debug from 'debug' debug('rerum_server_nodejs:server') import http from "http" -import dotenv from "dotenv" -dotenv.config() +import config from '../config/index.js' /** * Get port from environment and store in Express. */ -const port = process.env.PORT ?? 3001 +const port = config.PORT ?? 3001 app.set('port', port) /** diff --git a/config/index.js b/config/index.js new file mode 100644 index 00000000..37ed7120 --- /dev/null +++ b/config/index.js @@ -0,0 +1,33 @@ +/** + * Centralized environment configuration for the RERUM API. + * Loads variables from .env via dotenv and provides typed defaults. + * All modules should import config from this file instead of + * reading process.env directly. + * + * @module config + * @author joeljoby02 + */ +import dotenv from 'dotenv' +dotenv.config() + +const config = { + MONGO_CONNECTION_STRING: process.env.MONGO_CONNECTION_STRING ?? 'mongodb://localhost:27017', + MONGODBNAME: process.env.MONGODBNAME ?? 'rerum', + MONGODBCOLLECTION: process.env.MONGODBCOLLECTION ?? 'objects', + DOWN: process.env.DOWN ?? 'false', + READONLY: process.env.READONLY ?? 'false', + CLIENT_ID: process.env.CLIENT_ID ?? process.env.CLIENTID ?? '', + CLIENT_SECRET: process.env.CLIENT_SECRET ?? process.env.RERUMSECRET ?? '', + RERUM_PREFIX: process.env.RERUM_PREFIX ?? 'http://localhost:3001/v1/', + RERUM_ID_PREFIX: process.env.RERUM_ID_PREFIX ?? 'http://localhost:3001/v1/id/', + RERUM_AGENT_CLAIM: process.env.RERUM_AGENT_CLAIM ?? 'http://localhost:3001/agent', + RERUM_CONTEXT: process.env.RERUM_CONTEXT ?? 'http://localhost:3001/v1/context.json', + RERUM_API_VERSION: process.env.RERUM_API_VERSION ?? '1.0.0', + BOT_AGENT: process.env.BOT_AGENT ?? '', + AUDIENCE: process.env.AUDIENCE ?? '', + ISSUER_BASE_URL: process.env.ISSUER_BASE_URL ?? '', + BOT_TOKEN: process.env.BOT_TOKEN ?? '', + PORT: parseInt(process.env.PORT ?? process.env.PORT_NUMBER ?? 3001, 10) +} + +export default config diff --git a/controllers/bulk.js b/controllers/bulk.js index 35e7fcb5..9fb78e6d 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -6,8 +6,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' /** @@ -75,13 +76,13 @@ const bulkCreate = async function (req, res, next) { // id is also protected in this case, so it can't be set. if(_contextid(d["@context"])) delete d.id d._id = id - d['@id'] = `${process.env.RERUM_ID_PREFIX}${id}` + d['@id'] = `${config.RERUM_ID_PREFIX}${id}` bulkOps.push({ insertOne : { "document" : d }}) } try { let dbResponse = await db.bulkWrite(bulkOps, {'ordered':false}) res.set("Content-Type", "application/json; charset=utf-8") - res.set("Link",dbResponse.result.insertedIds.map(r => `${process.env.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 + res.set("Link",dbResponse.result.insertedIds.map(r => `${config.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 res.status(201) const estimatedResults = bulkOps.map(f=>{ let doc = f.insertOne.document @@ -148,7 +149,7 @@ const bulkUpdate = async function (req, res, next) { // Update the same thing twice? can vs should. // if(encountered.includes(idReceived)) continue encountered.push(idReceived) - if(!idReceived.includes(process.env.RERUM_ID_PREFIX)) continue + if(!idReceived.includes(config.RERUM_ID_PREFIX)) continue let id = parseDocumentID(idReceived) let originalObject try { @@ -168,7 +169,7 @@ const bulkUpdate = async function (req, res, next) { // id is also protected in this case, so it can't be set. if(_contextid(objectReceived["@context"])) delete objectReceived.id delete objectReceived["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) bulkOps.push({ insertOne : { "document" : newObject }}) if(originalObject.__rerum.history.next.indexOf(newObject["@id"]) === -1){ originalObject.__rerum.history.next.push(newObject["@id"]) @@ -185,7 +186,7 @@ const bulkUpdate = async function (req, res, next) { try { let dbResponse = await db.bulkWrite(bulkOps, {'ordered':false}) res.set("Content-Type", "application/json; charset=utf-8") - res.set("Link", dbResponse.result.insertedIds.map(r => `${process.env.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 + res.set("Link", dbResponse.result.insertedIds.map(r => `${config.RERUM_ID_PREFIX}${r._id}`)) // https://www.rfc-editor.org/rfc/rfc5988 res.status(200) const estimatedResults = bulkOps.filter(f=>f.insertOne).map(f=>{ let doc = f.insertOne.document diff --git a/controllers/crud.js b/controllers/crud.js index 7702de58..6bf70000 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -4,8 +4,9 @@ * Basic CRUD operations for RERUM v1 * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, idNegotiation, generateSlugId, ObjectID, createExpressError, getAgentClaim, parseDocumentID } from './utils.js' /** @@ -42,7 +43,7 @@ const create = async function (req, res, next) { if(_contextid(provided["@context"])) delete provided.id delete provided["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, provided, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, provided, rerumProp, { "_id": id }) console.log("CREATE") try { let result = await db.insertOne(newObject) diff --git a/controllers/delete.js b/controllers/delete.js index 12aec2ac..2e9737bf 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -4,8 +4,9 @@ * Delete operations for RERUM v1 * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { createExpressError, getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } from './utils.js' /** @@ -159,7 +160,7 @@ async function healHistoryTree(obj) { throw Error("Could not update all descendants with their new prime value") } } - if (previous_id.indexOf(process.env.RERUM_PREFIX) > -1) { + if (previous_id.indexOf(config.RERUM_PREFIX) > -1) { let previousIdForQuery = parseDocumentID(previous_id) const objToUpdate2 = await db.findOne({"$or":[{"_id": previousIdForQuery}, {"__rerum.slug": previousIdForQuery}]}) if (null !== objToUpdate2) { diff --git a/controllers/gog.js b/controllers/gog.js index 67dd04de..895ccaba 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -6,7 +6,7 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' diff --git a/controllers/history.js b/controllers/history.js index f0ad0031..0ea13df8 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -6,7 +6,7 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' diff --git a/controllers/overwrite.js b/controllers/overwrite.js index 284fac89..5371f58a 100644 --- a/controllers/overwrite.js +++ b/controllers/overwrite.js @@ -6,7 +6,7 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' diff --git a/controllers/patchSet.js b/controllers/patchSet.js index 85e97af8..1cf8234c 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -6,8 +6,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** @@ -86,7 +87,7 @@ const patchSet = async function (req, res, next) { delete patchedObject["_id"] delete patchedObject["@id"] delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) try { let result = await db.insertOne(newObject) if (alterHistoryNext(originalObject, newObject["@id"])) { diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index c4cf53d7..28ba597b 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -6,8 +6,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** @@ -90,7 +91,7 @@ const patchUnset = async function (req, res, next) { // id is also protected in this case, so it can't be set. if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) console.log("PATCH UNSET") try { let result = await db.insertOne(newObject) diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index c7271bbb..c0863e7c 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -6,8 +6,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** @@ -89,7 +90,7 @@ const patchUpdate = async function (req, res, next) { // id is also protected in this case, so it can't be set. if(_contextid(patchedObject["@context"])) delete patchedObject.id delete patchedObject["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, patchedObject, rerumProp, { "_id": id }) console.log("PATCH UPDATE") try { let result = await db.insertOne(newObject) diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index 177507ac..3c42deb2 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -6,8 +6,9 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, alterHistoryNext } from './utils.js' /** @@ -26,7 +27,7 @@ const putUpdate = async function (req, res, next) { let generatorAgent = getAgentClaim(req, next) const idReceived = objectReceived["@id"] ?? objectReceived.id if (idReceived) { - if(!idReceived.includes(process.env.RERUM_ID_PREFIX)){ + if(!idReceived.includes(config.RERUM_ID_PREFIX)){ //This is not a regular update. This object needs to be imported, it isn't in RERUM yet. return _import(req, res, next) } @@ -62,7 +63,7 @@ const putUpdate = async function (req, res, next) { if(_contextid(objectReceived["@context"])) delete objectReceived.id delete objectReceived["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) console.log("UPDATE") try { let result = await db.insertOne(newObject) @@ -121,7 +122,7 @@ async function _import(req, res, next) { if(_contextid(objectReceived["@context"])) delete objectReceived.id delete objectReceived["@context"] - let newObject = Object.assign(context, { "@id": process.env.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) + let newObject = Object.assign(context, { "@id": config.RERUM_ID_PREFIX + id }, objectReceived, rerumProp, { "_id": id }) console.log("IMPORT") try { let result = await db.insertOne(newObject) diff --git a/controllers/release.js b/controllers/release.js index 62f26f04..5d7f5f87 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -6,7 +6,7 @@ * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation, generateSlugId, establishReleasesTree, healReleasesTree } from './utils.js' diff --git a/controllers/search.js b/controllers/search.js index 5a688abf..dc5ec750 100644 --- a/controllers/search.js +++ b/controllers/search.js @@ -4,7 +4,7 @@ * Search ($search) operations for RERUM v1 * @author thehabes */ -import { db } from '../database/index.js' +import { db } from '../database/client.js' import utils from '../utils.js' import { idNegotiation, createExpressError } from './utils.js' diff --git a/controllers/utils.js b/controllers/utils.js index 9da47cea..5d383ded 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -4,8 +4,9 @@ * Utility functions for RERUM controllers * @author Claude Sonnet 4, cubap, thehabes */ -import { newID, isValidID, db } from '../database/index.js' +import { newID, isValidID, db } from '../database/client.js' import utils from '../utils.js' +import config from '../config/index.js' const ObjectID = newID @@ -57,7 +58,7 @@ const idNegotiation = function (resBody) { if(_contextid(resBody["@context"])) { delete resBody["@id"] delete resBody["@context"] - modifiedResBody = Object.assign(context, { "id": process.env.RERUM_ID_PREFIX + _id }, resBody) + modifiedResBody = Object.assign(context, { "id": config.RERUM_ID_PREFIX + _id }, resBody) } return modifiedResBody } @@ -144,7 +145,7 @@ const remove = async function(id) { * The app is forbidden until registered with RERUM. Access tokens are encoded with the agent. */ function getAgentClaim(req, next) { - const claimKeys = [process.env.RERUM_AGENT_CLAIM, "http://devstore.rerum.io/v1/agent", "http://store.rerum.io/agent"] + const claimKeys = [config.RERUM_AGENT_CLAIM, "http://devstore.rerum.io/v1/agent", "http://store.rerum.io/agent"] let agent = "" for (const claimKey of claimKeys) { agent = req.user[claimKey] diff --git a/database/client.js b/database/client.js new file mode 100644 index 00000000..3b167ff5 --- /dev/null +++ b/database/client.js @@ -0,0 +1,47 @@ +/** + * Centralized MongoDB client for the RERUM API. + * Provides a single shared MongoClient instance, connection + * management, and collection access for the application. + * + * @module database/client + * @author joeljoby02 + */ +import { MongoClient, ObjectId } from 'mongodb' +import config from '../config/index.js' + +// Single shared Mongo client for the entire application +const client = new MongoClient(config.MONGO_CONNECTION_STRING) + +// connect immediately; callers may import `connect` if they want to await it +const connect = async () => { + await client.connect() + console.dir({ + db: config.MONGODBNAME, + coll: config.MONGODBCOLLECTION + }) +} + +// collection helper +const db = client.db(config.MONGODBNAME)?.collection(config.MONGODBCOLLECTION) + +// simple utilities previously scattered in index.js +const newID = () => new ObjectId().toHexString() +const isValidID = (id) => ObjectId.isValid(id) + +const connected = async function () { + await client.db('admin').command({ ping: 1 }).catch(err => err) + return true +} + +// ensure connection is attempted at module load time (as before) +connect().catch(console.dir) + +export { + client, + connect, + db, + newID, + isValidID, + connected, + ObjectId +} diff --git a/database/index.js b/database/index.js index cf8d374a..39c444dd 100644 --- a/database/index.js +++ b/database/index.js @@ -1,57 +1,20 @@ -import { MongoClient, ObjectId } from 'mongodb' -import dotenv from "dotenv" -dotenv.config() - -const client = new MongoClient(process.env.MONGO_CONNECTION_STRING) -const newID = () => new ObjectId().toHexString() -const isValidID = (id) => ObjectId.isValid(id) -const connected = async function () { - // Send a ping to confirm a successful connection - await client.db("admin").command({ ping: 1 }).catch(err => err) - return true -} -const db = client.db(process.env.MONGODBNAME)?.collection(process.env.MONGODBCOLLECTION) -const connect = async () => { - await client.connect() - console.dir({ - db : process.env.MONGODBNAME, - coll : process.env.MONGODBCOLLECTION - }) -} -connect().catch(console.dir) - /** - * Find a single record based on a query object. - * @param {JSON} matchDoc Query Object to match properties. - * @param {JSON} options Just mongodb passthru for now - * @param {function} callback Callback function if needed - * @returns Single matched document or `null` if there is none found. - * @throws MongoDB error if matchDoc is malformed or server is unreachable; E11000 duplicate key error collection + * Database module backward compatibility layer. + * + * This module re-exports all symbols from database/client.js for backward + * compatibility with legacy code. New code should import directly from + * database/client.js instead. This layer maintains a single entry point + * for any external consumers but does not add new functionality. + * + * @module database/index */ -function getMatching(matchDoc, options, callback) { - return db.findOne(matchDoc, options, (err, doc) => { - if (typeof callback === 'function') return callback(err, doc) - if (err) throw err - return doc - }) -} - -function isObject(obj) { - return obj?.constructor == Object -} - -function isValidURL(url) { - try { - new URL(url) - return true - } catch (_) { - return false - } -} export { + client, + connect, + db, newID, isValidID, connected, - db -} + ObjectId +} from './client.js' diff --git a/index.js b/index.js new file mode 100644 index 00000000..2661876e --- /dev/null +++ b/index.js @@ -0,0 +1,55 @@ +import http from 'http' +import app from './app.js' + +/** + * Express application instance used throughout the project. Exported + * primarily for testing or embedding inside another server. + * + * ```js + * import { app } from 'rerum_server' + * ``` + */ +export { app } + +/** + * Default export is the express app largely for backwards compatibility + * with consumers that do `import app from 'rerum_server'`. + */ +export default app + +/** + * Helper that creates an HTTP server for the configured express app. + * The returned server is **not** listening yet; caller may attach + * additional listeners or configure timeouts before calling + * `server.listen(...)`. + * + * @param {number|string} [port=process.env.PORT||3001] port to assign to + * the express app and eventually listen on + * @returns {import('http').Server} http server instance + */ +export function createServer(port = process.env.PORT ?? 3001) { + app.set('port', port) + const server = http.createServer(app) + + server.keepAliveTimeout = 8 * 1000 + server.headersTimeout = 8.5 * 1000 + + return server +} + +/** + * Convenience function to start the server immediately. Returns the + * server instance so callers can close it in tests or hook events. + * + * @param {number|string} [port] optional port override + * @returns {import('http').Server} + */ +export function start(port) { + const p = port ?? process.env.PORT ?? 3001 + const server = createServer(p) + server.listen(p) + server.on('listening', () => { + console.log('LISTENING ON ' + p) + }) + return server +} diff --git a/package.json b/package.json index 30f205b0..33d5d063 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "rerum_server_nodejs", "type": "module", "version": "0.0.0", - "private": true, + "main": "./app.js", "description": "Rerum API server for database access.", "keywords": [ "rerum", @@ -18,15 +18,22 @@ "homepage": "https://store.rerum.io", "license": "UNLICENSED", "author": "Research Computing Group (https://slu.edu)", - "repository": "github:CenterForDigitalHumanities/rerum_server_nodejs", + "repository": { + "type": "git", + "url": "git+https://github.com/CenterForDigitalHumanities/rerum_server_nodejs.git" + }, + "bugs": { + "url": "https://github.com/CenterForDigitalHumanities/rerum_server_nodejs/issues" + }, "engines": { "node": ">=24.12.0", "npm": ">=11.7.0" }, + "main": "index.js", "scripts": { "start": "node ./bin/rerum_v1.js", - "test": "jest", - "runtest": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "runtest": "npm test" }, "dependencies": { "cookie-parser": "~1.4.7", diff --git a/public/index.html b/public/index.html index 28eee8c7..10692e6e 100644 --- a/public/index.html +++ b/public/index.html @@ -18,18 +18,19 @@ color: green; font-weight: bold; } - + #intro { color: #979A9E; font-size: 12pt; } - + body { font-family: 'Open Sans', sans-serif; color: #979A9E; background-color: #2F353E; + padding: 20px; } - + input[type="text"] { background-color: #ccc; color: black; @@ -37,48 +38,58 @@ font-family: serif; font-size: 14pt; } - + + .column { + float: left; + width: 33%; + } + + .column2 { + float: right; + width: 9%; + } + h1 { cursor: pointer; font-weight: 300; font-family: 'Raleway', sans-serif; margin-bottom: 10px; } - + .navbar-brand { float: none; font-size: 2rem; line-height: 1.5; margin-bottom: 20px; } - + #login { display: none; } - + .panel-body { color: initial; } - + .panel { word-break: break-word; } - + .status_header { color: gray; } - + #a_t { height: 170px; margin-bottom: 8px; } - + #a_t, #r_t_4_a_t, #new_refresh_token { margin-bottom: 8px; } - + #code_for_refresh_token { margin-bottom: -13px; } @@ -106,287 +117,424 @@

-
Application Registration
-
-

- Interacting with RERUM requires server-to-server communication, so we suggest the registrant be the - application developer. - You may want to - learn more about the concepts around RERUM - before reading the API. -

-

- If you are here for the first time and think you want to use RERUM, please - read the API first. -

-

- If you like what you read in our API documentation - and want to begin using RERUM as a back stack service please register by clicking below. - Be prepared to be routed to Auth0 (don't know why? - Read the API). -

-

- After registering, you will be returned to this page with an Auth0 Authorization code. Use that code at - the bottom of this page to get a refresh token - and an access token so you can use the API. You may notice the page has already populated known - information for you. -

-
- - -
-
Auth0 Authorization Status
-
-

- If you believe you are already registered and want to check on your status, follow the prompts below. - You will be routed to Auth0 so we can verify who you are. -

-
- Auth0 Status - UNKNOWN + +
+
+
+
Application Registration
+
+

+ Interacting with RERUM requires server-to-server communication, so we suggest the registrant be the + application developer. + You may want to + learn more about the concepts around RERUM + before reading the API. +

+

+ If you are here for the first time and think you want to use RERUM, please + read the API first. +

+

+ If you like what you read in our API documentation + and want to begin using RERUM as a back stack service please register by clicking below. + Be prepared to be routed to Auth0 (don't know why? + Read the API). +

+

+ After registering, you will be returned to this page with an Auth0 Authorization code. +

+
+
- -
-
Test RERUM API Access
-
-

- Provide your access token below to check if it is still valid. If so, your access to RERUM will be - authorized. Otherwise, you will see an "unauthorized" message. -

-

- If the token you have is not working, it may be because access tokens expire every 30 days. You can use - your refresh token to get a new access token. -

- -
- RERUM status - UNKNOWN + + -
-
Get A New Access Token
-
-

- Your access token to use RERUM expires every 30 days. Has it been that long or longer? Provide your - refresh token below to get a new access token. - If you lost your refresh token, you can get a new one in "Get A New Refresh Token" below. -

- -
- Status - UNKNOWN +
+
Get A New Refresh Token
+
+

+ You can supply a valid Auth0 Authorization Code to get a new refresh token. Use "Check my Authorization + Status with Auth0" to get a valid code. +

+ Enter your code: +
+ + +
+
+ Status + UNKNOWN +
+
+ +
+
+ +
+
+
+
- -
-
Get A New Refresh Token
-
-

- You can supply a valid Auth0 Authorization Code to get a new refresh token. Use "Check my Authorization - Status with Auth0" to get a valid code. -

- Enter your code: -
- -
- Status - UNKNOWN +
+
Get A New Access Token
+
+

+ Your access token to use RERUM expires every 30 days. Has it been that long or longer? Provide your + refresh token below to get a new access token. + If you lost your refresh token, you can get a new one in "Get A New Refresh Token" below. + The generated access token will be displayed in the box above +

+ +
+ Status + UNKNOWN +
+
+
- + diff --git a/rest.js b/rest.js index 187535bd..275d9496 100644 --- a/rest.js +++ b/rest.js @@ -19,6 +19,8 @@ * * The error handler sits a level up, so do not res.send() here. Just give back a boolean */ +import config from './config/index.js' + const checkPatchOverrideSupport = function (req, res) { const override = req.header("X-HTTP-Method-Override") return undefined !== override && override === "PATCH" @@ -68,7 +70,7 @@ Token: ${token} ` else { error.message += ` The request does not contain an "Authorization" header and so is Unauthorized. Please include a token with your requests -like "Authorization: Bearer ". Make sure you have registered at ${process.env.RERUM_PREFIX}.` +like "Authorization: Bearer ". Make sure you have registered at ${config.RERUM_PREFIX}.` } break case 403: @@ -80,9 +82,9 @@ Token: ${token}` } else { //If there was no Token, this would be a 401. If you made it here, you didn't REST. - err.message += ` + error.message += ` You are Forbidden from performing this action. The request does not contain an "Authorization" header. -Make sure you have registered at ${process.env.RERUM_PREFIX}. ` +Make sure you have registered at ${config.RERUM_PREFIX}. ` } case 404: error.message += ` diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 788247f9..320f175c 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -1,7 +1,7 @@ import { jest } from "@jest/globals" import express from "express" import request from "supertest" -import { db } from '../../database/index.js' +import { db } from '../../database/client.js' import controller from '../../db-controller.js' const rerum_uri = `${process.env.RERUM_ID_PREFIX}123456` diff --git a/routes/__tests__/crud_routes_function.txt b/routes/__tests__/crud_routes_function.txt index 511c3caa..da22d6de 100644 --- a/routes/__tests__/crud_routes_function.txt +++ b/routes/__tests__/crud_routes_function.txt @@ -8,7 +8,7 @@ import request from 'supertest' //Fun fact, if you don't require app, you don't get coverage even though the tests run just fine. -import app from '../../app.js' +import app from '../../index.js' // use public API instead of deep path //This is so we can do Mongo specific things with the objects in this test, like actually remove them from the db. import controller from '../../db-controller.js' diff --git a/routes/__tests__/overwrite-optimistic-locking.test.txt b/routes/__tests__/overwrite-optimistic-locking.test.txt index 3ef6486e..fed385e0 100644 --- a/routes/__tests__/overwrite-optimistic-locking.test.txt +++ b/routes/__tests__/overwrite-optimistic-locking.test.txt @@ -7,7 +7,7 @@ const mockFindOne = jest.fn() const mockReplaceOne = jest.fn() // Mock the database module -jest.mock('../../database/index.js', () => ({ +jest.mock('../../database/client.js', () => ({ db: { findOne: mockFindOne, replaceOne: mockReplaceOne diff --git a/routes/__tests__/overwrite.test.txt b/routes/__tests__/overwrite.test.txt index 129d7ea0..5bff29e8 100644 --- a/routes/__tests__/overwrite.test.txt +++ b/routes/__tests__/overwrite.test.txt @@ -1,5 +1,5 @@ import request from 'supertest' -import app from '../../app.js' +import app from '../../index.js' // public entry point import { jest } from '@jest/globals' // Mock the database and auth modules diff --git a/routes/client.js b/routes/client.js index 0713ce68..9bc60f35 100644 --- a/routes/client.js +++ b/routes/client.js @@ -2,17 +2,18 @@ import express from 'express' const router = express.Router() import auth from '../auth/index.js' import { getAgentClaim } from '../controllers/utils.js' +import config from '../config/index.js' router.get('/register', (req, res, next) => { //Register means register with the RERUM Server Auth0 client and get a new code for a refresh token. //See https://auth0.com/docs/libraries/custom-signup const params = new URLSearchParams({ - "audience":process.env.AUDIENCE, - "scope":"offline_access", - "response_type":"code", - "client_id":process.env.CLIENT_ID, - "redirect_uri":process.env.RERUM_PREFIX, - "state":"register" + "audience": config.AUDIENCE, + "scope": "offline_access", + "response_type": "code", + "client_id": config.CLIENT_ID, + "redirect_uri": config.RERUM_PREFIX, + "state": "register" }).toString() res.status(200).send(`https://cubap.auth0.com/authorize?${params}`) }) diff --git a/utils.js b/utils.js index 37b36b7a..a20121c5 100644 --- a/utils.js +++ b/utils.js @@ -6,6 +6,7 @@ * * @author thehabes */ +import config from './config/index.js' /** * Add the __rerum properties object to a given JSONObject.If __rerum already exists, it will be overwritten because this method is only called on new objects. Properties for consideration are: @@ -82,9 +83,9 @@ const configureRerumOptions = function(generator, received, update, extUpdate){ history.next = [] history.previous = history_previous history.prime = history_prime - rerumOptions["@context"] = process.env.RERUM_CONTEXT + rerumOptions["@context"] = config.RERUM_CONTEXT rerumOptions.alpha = true - rerumOptions.APIversion = process.env.RERUM_API_VERSION + rerumOptions.APIversion = config.RERUM_API_VERSION //It is important for the cache workflow that these be properly formatted. let creationDateTime = new Date(Date.now()).toISOString().replace("Z", "") rerumOptions.createdAt = creationDateTime