Browse Source

feat: Install feathers server

Owen Diffey 5 months ago
parent
commit
d311972dc5

+ 121 - 0
server/.gitignore

@@ -0,0 +1,121 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+.env.production
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+.sqlite
+
+lib/

+ 9 - 0
server/.prettierrc

@@ -0,0 +1,9 @@
+{
+  "tabWidth": 2,
+  "useTabs": false,
+  "printWidth": 110,
+  "semi": false,
+  "trailingComma": "none",
+  "singleQuote": true,
+  "parser": "typescript"
+}

+ 10 - 0
server/config/custom-environment-variables.json

@@ -0,0 +1,10 @@
+{
+  "port": {
+    "__name": "PORT",
+    "__format": "number"
+  },
+  "host": "HOSTNAME",
+  "authentication": {
+    "secret": "FEATHERS_SECRET"
+  }
+}

+ 16 - 0
server/config/default.json

@@ -0,0 +1,16 @@
+{
+  "host": "localhost",
+  "port": 3030,
+  "public": "./public/",
+  "origins": [
+    "http://localhost:3030"
+  ],
+  "paginate": {
+    "default": 10,
+    "max": 50
+  },
+  "postgresql": {
+    "client": "pg",
+    "connection": "postgres://musare:PASSWORD@postgres:5432/musare"
+  }
+}

+ 3 - 0
server/config/test.json

@@ -0,0 +1,3 @@
+{
+  "port": 8998
+}

+ 7 - 0
server/knexfile.ts

@@ -0,0 +1,7 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/databases.html
+import { app } from './src/app'
+
+// Load our database connection info from the app configuration
+const config = app.get('postgresql')
+
+module.exports = config

+ 79 - 0
server/package.json

@@ -0,0 +1,79 @@
+{
+  "name": "server",
+  "description": "An open-source collaborative music listening and catalogue curation application.",
+  "version": "0.0.0",
+  "homepage": "",
+  "private": true,
+  "keywords": [
+    "feathers"
+  ],
+  "author": {},
+  "contributors": [],
+  "bugs": {},
+  "engines": {
+    "node": ">= 20.14.0"
+  },
+  "feathers": {
+    "language": "ts",
+    "packager": "npm",
+    "database": "postgresql",
+    "framework": "koa",
+    "transports": [
+      "rest",
+      "websockets"
+    ],
+    "schema": "typebox"
+  },
+  "directories": {
+    "lib": "src",
+    "test": "test"
+  },
+  "files": [
+    "lib/client.js",
+    "lib/**/*.d.ts",
+    "lib/**/*.shared.js"
+  ],
+  "main": "lib/client",
+  "scripts": {
+    "dev": "nodemon -x ts-node src/index.ts",
+    "compile": "shx rm -rf lib/ && tsc",
+    "start": "node lib/",
+    "prettier": "npx prettier \"**/*.ts\" --write",
+    "mocha": "cross-env NODE_ENV=test mocha test/ --require ts-node/register --recursive --extension .ts --exit",
+    "test": "cross-env NODE_ENV=test npm run migrate && npm run mocha",
+    "bundle:client": "npm run compile && npm pack --pack-destination ./public",
+    "migrate": "knex migrate:latest",
+    "migrate:make": "knex migrate:make"
+  },
+  "dependencies": {
+    "@feathersjs/adapter-commons": "^5.0.31",
+    "@feathersjs/authentication": "^5.0.31",
+    "@feathersjs/authentication-client": "^5.0.31",
+    "@feathersjs/configuration": "^5.0.31",
+    "@feathersjs/errors": "^5.0.31",
+    "@feathersjs/feathers": "^5.0.31",
+    "@feathersjs/knex": "^5.0.31",
+    "@feathersjs/koa": "^5.0.31",
+    "@feathersjs/schema": "^5.0.31",
+    "@feathersjs/socketio": "^5.0.31",
+    "@feathersjs/transport-commons": "^5.0.31",
+    "@feathersjs/typebox": "^5.0.31",
+    "knex": "^3.1.0",
+    "pg": "^8.13.1",
+    "winston": "^3.17.0"
+  },
+  "devDependencies": {
+    "@feathersjs/cli": "^5.0.31",
+    "@feathersjs/rest-client": "^5.0.31",
+    "@types/mocha": "^10.0.9",
+    "@types/node": "^22.9.0",
+    "axios": "^1.7.7",
+    "cross-env": "^7.0.3",
+    "mocha": "^10.8.2",
+    "nodemon": "^3.1.7",
+    "prettier": "^3.3.3",
+    "shx": "^0.3.4",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.6.3"
+  }
+}

+ 37 - 0
server/public/index.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>server</title>
+    <meta name="description" content="An open-source collaborative music listening and catalogue curation application.">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+        box-sizing: border-box;
+      }
+
+      html {
+        height: 100%;
+      }
+
+      body {
+        min-height: 100%;
+        display: flex;
+        align-items: center;
+      }
+
+      img.logo {
+        display: block;
+        margin: auto auto;
+        width: 30%;
+        max-width: 100%;
+        max-height: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <img class="logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEyOCA5LjEwMmM2NS42NjUgMCAxMTguODk4IDUzLjIzMyAxMTguODk4IDExOC44OTggMCA2NS42NjUtNTMuMjMzIDExOC44OTgtMTE4Ljg5OCAxMTguODk4QzYyLjMzNSAyNDYuODk4IDkuMTAyIDE5My42NjUgOS4xMDIgMTI4IDkuMTAyIDYyLjMzNSA2Mi4zMzUgOS4xMDIgMTI4IDkuMTAyTTEyOCAwQzU3LjQyMSAwIDAgNTcuNDIxIDAgMTI4YzAgNzAuNTc5IDU3LjQyMSAxMjggMTI4IDEyOCA3MC41NzkgMCAxMjgtNTcuNDIxIDEyOC0xMjhDMjU2IDU3LjQyMSAxOTguNTc5IDAgMTI4IDBtMjAuODMgMjUuNTI0Yy0xMC40My0xLjg5Ni0zNS42NTEgMzYuNDA5LTQzLjk5NCA1OS43MzQtLjYzNCAxLjc2OS0yLjA4NiA4LjI0OS0yLjA4NiA5Ljk1NSAwIDAgNi41MzEgMTQuMDU1IDguMzQzIDE3LjM1MS0zLjAzNC0xLjU4LTkuMzIzLTEzLjc1Ni05LjMyMy0xMy43NTYtMy4wMzQgNS43ODQtNS45NDIgMzIuMzQtNC45OTQgMzcuMjcxIDAgMCA2Ljc2MiAxMC4wNjIgOS4zODcgMTIuNTc4LTMuNjAzLTEuMjAxLTkuNjcxLTkuMzU1LTkuNjcxLTkuMzU1LTEuMTM4IDMuNTA4LS45MTYgMTAuODA3LS4zNzkgMTMuMjc0IDQuNTUxIDYuNjM3IDEwLjYxOSA3LjM5NiAxMC42MTkgNy4zOTZzLTYuNjM3IDY2LjE4MSAzLjQxMyA3MS4xMTFjNi4yNTgtMS4zMjcgNy43NzUtNzMuOTU2IDcuNzc1LTczLjk1NnM3LjU4NS41NjkgOS4yOTItMS4zMjdjMy44NTYtMi42NTUgMTIuODI2LTMwLjIyNCAxMi45NTgtMzQuMjAyIDAgMC0xMC40MSAxLjk1Mi0xNS40ODcgMy45MjQgMy44MjYtMy44IDE2LjA0OS02LjM1MiAxNi4wNDktNi4zNTIgMy4zMTUtMy45NzkgMTAuMjkxLTMxLjA0NyAxMC45OTQtMzkuMzkxLjE3Ni0yLjA5My41ODMtNC42NTcuMjY4LTguMzk4IDAgMC05Ljk0MSAyLjE3Ny0xMi4wMTQgMS40MjQgMi4xMDQtLjIzNyAxMi4yNjMtNC4xNCAxMi4yNjMtNC4xNCAxLjgwMS0xNi4yMTMgMi4zNTgtNDIuMDkxLTMuNDEzLTQzLjE0MXptLTM2LjM4IDE3MS42OTFjLS43OTUgMTkuNDk2LTEuMjk0IDI1LjAwNC0yLjExNSAyOS42MDEtLjM3OS44NTctLjc1OC45OTctMS4xMzgtLjA5NS0zLjQ3Ny0xNS45OTItMy4yMjQtMTM2LjQzOCAzNi40MDktMTkxLjI0MS0yMy4wNSA0Mi4wOTItMzMuNTM1IDEyMi44NjEtMzMuMTU2IDE2MS43MzV6IiBmaWxsPSIjMzMzIi8+PC9zdmc+" />
+  </body>
+</html>
+

+ 42 - 0
server/readme.md

@@ -0,0 +1,42 @@
+# server
+
+> An open-source collaborative music listening and catalogue curation application.
+
+## About
+
+This project uses [Feathers](http://feathersjs.com). An open source framework for building APIs and real-time applications.
+
+## Getting Started
+
+1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed.
+2. Install your dependencies
+
+    ```
+    cd path/to/server
+    npm install
+    ```
+
+3. Start your app
+
+    ```
+    npm run compile # Compile TypeScript source
+    npm run migrate # Run migrations to set up the database
+    npm start
+    ```
+
+## Testing
+
+Run `npm test` and all your tests in the `test/` directory will be run.
+
+## Scaffolding
+
+This app comes with a powerful command line interface for Feathers. Here are a few things it can do:
+
+```
+$ npx feathers help                           # Show all commands
+$ npx feathers generate service               # Generate a new Service
+```
+
+## Help
+
+For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com).

+ 54 - 0
server/src/app.ts

@@ -0,0 +1,54 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/application.html
+import { feathers } from '@feathersjs/feathers'
+import configuration from '@feathersjs/configuration'
+import { koa, rest, bodyParser, errorHandler, parseAuthentication, cors, serveStatic } from '@feathersjs/koa'
+import socketio from '@feathersjs/socketio'
+
+import { configurationValidator } from './configuration'
+import type { Application } from './declarations'
+import { logError } from './hooks/log-error'
+import { postgresql } from './postgresql'
+import { services } from './services/index'
+import { channels } from './channels'
+
+const app: Application = koa(feathers())
+
+// Load our app configuration (see config/ folder)
+app.configure(configuration(configurationValidator))
+
+// Set up Koa middleware
+app.use(cors())
+app.use(serveStatic(app.get('public')))
+app.use(errorHandler())
+app.use(parseAuthentication())
+app.use(bodyParser())
+
+// Configure services and transports
+app.configure(rest())
+app.configure(
+  socketio({
+    cors: {
+      origin: app.get('origins')
+    }
+  })
+)
+app.configure(postgresql)
+app.configure(services)
+app.configure(channels)
+
+// Register hooks that run on all service methods
+app.hooks({
+  around: {
+    all: [logError]
+  },
+  before: {},
+  after: {},
+  error: {}
+})
+// Register application setup and teardown hooks here
+app.hooks({
+  setup: [],
+  teardown: []
+})
+
+export { app }

+ 38 - 0
server/src/channels.ts

@@ -0,0 +1,38 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/channels.html
+import type { RealTimeConnection, Params } from '@feathersjs/feathers'
+import type { AuthenticationResult } from '@feathersjs/authentication'
+import '@feathersjs/transport-commons'
+import type { Application, HookContext } from './declarations'
+import { logger } from './logger'
+
+export const channels = (app: Application) => {
+  logger.warn(
+    'Publishing all events to all authenticated users. See `channels.ts` and https://dove.feathersjs.com/api/channels.html for more information.'
+  )
+
+  app.on('connection', (connection: RealTimeConnection) => {
+    // On a new real-time connection, add it to the anonymous channel
+    app.channel('anonymous').join(connection)
+  })
+
+  app.on('login', (authResult: AuthenticationResult, { connection }: Params) => {
+    // connection can be undefined if there is no
+    // real-time connection, e.g. when logging in via REST
+    if (connection) {
+      // The connection is no longer anonymous, remove it
+      app.channel('anonymous').leave(connection)
+
+      // Add it to the authenticated user channel
+      app.channel('authenticated').join(connection)
+    }
+  })
+
+  // eslint-disable-next-line no-unused-vars
+  app.publish((data: any, context: HookContext) => {
+    // Here you can add event publishers to channels set up in `channels.js`
+    // To publish only for a specific event use `app.publish(eventname, () => {})`
+
+    // e.g. to publish all service events to all authenticated users use
+    return app.channel('authenticated')
+  })
+}

+ 34 - 0
server/src/client.ts

@@ -0,0 +1,34 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/client.html
+import { feathers } from '@feathersjs/feathers'
+import type { TransportConnection, Application } from '@feathersjs/feathers'
+import authenticationClient from '@feathersjs/authentication-client'
+import type { AuthenticationClientOptions } from '@feathersjs/authentication-client'
+
+export interface Configuration {
+  connection: TransportConnection<ServiceTypes>
+}
+
+export interface ServiceTypes {}
+
+export type ClientApplication = Application<ServiceTypes, Configuration>
+
+/**
+ * Returns a typed client for the server app.
+ *
+ * @param connection The REST or Socket.io Feathers client connection
+ * @param authenticationOptions Additional settings for the authentication client
+ * @see https://dove.feathersjs.com/api/client.html
+ * @returns The Feathers client application
+ */
+export const createClient = <Configuration = any,>(
+  connection: TransportConnection<ServiceTypes>,
+  authenticationOptions: Partial<AuthenticationClientOptions> = {}
+) => {
+  const client: ClientApplication = feathers()
+
+  client.configure(connection)
+  client.configure(authenticationClient(authenticationOptions))
+  client.set('connection', connection)
+
+  return client
+}

+ 17 - 0
server/src/configuration.ts

@@ -0,0 +1,17 @@
+import { Type, getValidator, defaultAppConfiguration } from '@feathersjs/typebox'
+import type { Static } from '@feathersjs/typebox'
+
+import { dataValidator } from './validators'
+
+export const configurationSchema = Type.Intersect([
+  defaultAppConfiguration,
+  Type.Object({
+    host: Type.String(),
+    port: Type.Number(),
+    public: Type.String()
+  })
+])
+
+export type ApplicationConfiguration = Static<typeof configurationSchema>
+
+export const configurationValidator = getValidator(configurationSchema, dataValidator)

+ 20 - 0
server/src/declarations.ts

@@ -0,0 +1,20 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html
+import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers'
+import { Application as FeathersApplication } from '@feathersjs/koa'
+import { ApplicationConfiguration } from './configuration'
+
+export type { NextFunction }
+
+// The types for app.get(name) and app.set(name)
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface Configuration extends ApplicationConfiguration {}
+
+// A mapping of service names to types. Will be extended in service files.
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface ServiceTypes {}
+
+// The application instance type that will be used everywhere else
+export type Application = FeathersApplication<ServiceTypes, Configuration>
+
+// The context for hook functions - can be typed with a service class
+export type HookContext<S = any> = FeathersHookContext<Application, S>

+ 18 - 0
server/src/hooks/log-error.ts

@@ -0,0 +1,18 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/log-error.html
+import type { HookContext, NextFunction } from '../declarations'
+import { logger } from '../logger'
+
+export const logError = async (context: HookContext, next: NextFunction) => {
+  try {
+    await next()
+  } catch (error: any) {
+    logger.error(error.stack)
+
+    // Log validation errors
+    if (error.data) {
+      logger.error('Data: %O', error.data)
+    }
+
+    throw error
+  }
+}

+ 11 - 0
server/src/index.ts

@@ -0,0 +1,11 @@
+import { app } from './app'
+import { logger } from './logger'
+
+const port = app.get('port')
+const host = app.get('host')
+
+process.on('unhandledRejection', (reason) => logger.error('Unhandled Rejection %O', reason))
+
+app.listen(port).then(() => {
+  logger.info(`Feathers app listening on http://${host}:${port}`)
+})

+ 10 - 0
server/src/logger.ts

@@ -0,0 +1,10 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/logging.html
+import { createLogger, format, transports } from 'winston'
+
+// Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston
+export const logger = createLogger({
+  // To see more detailed errors, change this to 'debug'
+  level: 'info',
+  format: format.combine(format.splat(), format.simple()),
+  transports: [new transports.Console()]
+})

+ 17 - 0
server/src/postgresql.ts

@@ -0,0 +1,17 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/databases.html
+import knex from 'knex'
+import type { Knex } from 'knex'
+import type { Application } from './declarations'
+
+declare module './declarations' {
+  interface Configuration {
+    postgresqlClient: Knex
+  }
+}
+
+export const postgresql = (app: Application) => {
+  const config = app.get('postgresql')
+  const db = knex(config!)
+
+  app.set('postgresqlClient', db)
+}

+ 6 - 0
server/src/services/index.ts

@@ -0,0 +1,6 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/application.html#configure-functions
+import type { Application } from '../declarations'
+
+export const services = (app: Application) => {
+  // All services will be registered here
+}

+ 29 - 0
server/src/validators.ts

@@ -0,0 +1,29 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/validators.html
+import { Ajv, addFormats } from '@feathersjs/schema'
+import type { FormatsPluginOptions } from '@feathersjs/schema'
+
+const formats: FormatsPluginOptions = [
+  'date-time',
+  'time',
+  'date',
+  'email',
+  'hostname',
+  'ipv4',
+  'ipv6',
+  'uri',
+  'uri-reference',
+  'uuid',
+  'uri-template',
+  'json-pointer',
+  'relative-json-pointer',
+  'regex'
+]
+
+export const dataValidator: Ajv = addFormats(new Ajv({}), formats)
+
+export const queryValidator: Ajv = addFormats(
+  new Ajv({
+    coerceTypes: true
+  }),
+  formats
+)

+ 40 - 0
server/test/app.test.ts

@@ -0,0 +1,40 @@
+// For more information about this file see https://dove.feathersjs.com/guides/cli/app.test.html
+import assert from 'assert'
+import axios from 'axios'
+import type { Server } from 'http'
+import { app } from '../src/app'
+
+const port = app.get('port')
+const appUrl = `http://${app.get('host')}:${port}`
+
+describe('Feathers application tests', () => {
+  let server: Server
+
+  before(async () => {
+    server = await app.listen(port)
+  })
+
+  after(async () => {
+    await app.teardown()
+  })
+
+  it('starts and shows the index page', async () => {
+    const { data } = await axios.get<string>(appUrl)
+
+    assert.ok(data.indexOf('<html lang="en">') !== -1)
+  })
+
+  it('shows a 404 JSON error', async () => {
+    try {
+      await axios.get(`${appUrl}/path/to/nowhere`, {
+        responseType: 'json'
+      })
+      assert.fail('should never get here')
+    } catch (error: any) {
+      const { response } = error
+      assert.strictEqual(response?.status, 404)
+      assert.strictEqual(response?.data?.code, 404)
+      assert.strictEqual(response?.data?.name, 'NotFound')
+    }
+  })
+})

+ 18 - 0
server/test/client.test.ts

@@ -0,0 +1,18 @@
+import assert from 'assert'
+import axios from 'axios'
+import type { Server } from 'http'
+import { app } from '../src/app'
+import { createClient } from '../src/client'
+
+import rest from '@feathersjs/rest-client'
+
+const port = app.get('port')
+const appUrl = `http://${app.get('host')}:${port}`
+
+describe('client tests', () => {
+  const client = createClient(rest(appUrl).axios(axios))
+
+  it('initialized the client', () => {
+    assert.ok(client)
+  })
+})

+ 21 - 0
server/tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "ts-node": {
+    "files": true
+  },
+  "compilerOptions": {
+    "target": "es2020",
+    "module": "commonjs",
+    "outDir": "./lib",
+    "rootDir": "./src",
+    "declaration": true,
+    "strict": true,
+    "esModuleInterop": true,
+    "sourceMap": true
+  },
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "test"
+  ]
+}