Skip to content

Commit

Permalink
convert from commonjs to es6/ecma modules
Browse files Browse the repository at this point in the history
- mysql: make a singleton, so process exits after tests
- mysql: add .delete and .whereConditions
- mysql.update added
- deps: updated vers
- doc(README): expand with curl session usage (#5)
- group.put added with tests
- test(group): use the test case
- user.put added
- permission: added with tests
- add config.getEnv
- ci: tests do not depend on lint
- ci: require node v20
- ci: fix test running on windows
- ci: only test on node v20
- eslint: switch to babel-eslint parser, b/c eslint doesn't yet support import..from..with syntax
  • Loading branch information
msimerson committed Feb 28, 2024
1 parent 8bda2c2 commit 3a2dfab
Show file tree
Hide file tree
Showing 39 changed files with 1,149 additions and 582 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
env:
node: true
es6: true
es2021: true
es2024: true
extends: eslint:recommended
parser: '@babel/eslint-parser'
parserOptions:
babelOptions:
configFile: false
plugins: [ "@babel/plugin-syntax-import-attributes" ]
requireConfigFile: false
ecmaVersion: latest
sourceType: module
rules: {}
19 changes: 12 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
- name: Start MySQL
run: sudo /etc/init.d/mysql start
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/checkout@v4
- run: npm install
- name: Initialize MySQL
Expand All @@ -43,12 +45,13 @@ jobs:
active: ${{ steps.get.outputs.active }}

test:
needs: [ lint, get-lts ]
needs: [ get-lts ]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
# node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
node-version: [ 20 ]
fail-fast: false
steps:
- run: sudo /etc/init.d/mysql start
Expand All @@ -61,11 +64,12 @@ jobs:
- run: npm test

test-mac:
needs: [ lint, get-lts ]
needs: [ get-lts ]
runs-on: macos-latest
strategy:
matrix:
node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
# node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
node-version: [ 20 ]
fail-fast: false
steps:
- name: Install & Start MySQL
Expand All @@ -84,11 +88,12 @@ jobs:

test-win:
# if: false
needs: [ lint, get-lts ]
needs: [ get-lts ]
runs-on: windows-latest
strategy:
matrix:
node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
node-version: [ 20 ]
# node-version: ${{ fromJson(needs.get-lts.outputs.active) }}
experimental: [true]
fail-fast: false
steps:
Expand All @@ -102,4 +107,4 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: sh sql/init-mysql.sh
- run: npm install
- run: npm test
- run: sh test.sh
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npm install

Edit the files in conf.d to reflect your local settings.

Each config file has a default section which lists all available config settings. Below the `default` section are optional deployment environments such as `production`, `development`, and `test`. When a config file is loaded, the environment variable `NODE_ENV` is checked and if defined, any overrides in the matching deployment section are applied.
Each config file has a default section which lists all available config settings. Below the `default` section are optional deployment environments such as `production`, `development`, and `test`. When a deployment environment is detected, overrides in the matching deployment section are applied.

## Start the service

Expand All @@ -33,5 +33,113 @@ or

`npm run develop (development)`

will start up the HTTP service on the port specified in conf.d/http.yml.
will start up the HTTP service on the port specified in `conf.d/http.yml`. The default URL for the service is [http://localhost:3000](http://localhost:3000) and the API methods have documentation at [http://localhost:3000/documentation](http://localhost:3000/documentation).


## Using the API service

Until the NicTool 3.0 HTTP client is written, using a web browser (in Developer mode) or a CLI HTTP utility like curl can be used. Here's a quick tutorial:

### Start a New Session

`curl -X POST http://localhost:3000/session`

```json
{"statusCode":400,"error":"Bad Request","message":"Invalid request payload input"}
```

The request was rejected because it's missing the required parameters, as shown in the documentation. Create a file called nt-auth.json and store the credentials of a NicTool user therein. Then try the auth request again:

`curl -X POST http://localhost:3000/session --header "Content-Type: application/json" -d @nt-auth.json`

```json
{"user":{"id":4096,"first_name":"Unit","last_name":"Test","username":"unit-test","email":"[email protected]"},"group":{"id":4096,"name":"example.com"},"session":{"id":162},"meta":{"api":{"version":"3.0.0"},"msg":"you are logged in"}
```

That's not the easiest to read so lets pipe it through `json_pp`:

`curl -X POST http://localhost:3000/session --header "Content-Type: application/json" -d @nt-auth.json | json_pp`

```json
{
"group" : {
"id" : 4096,
"name" : "example.com"
},
"meta" : {
"api" : {
"version" : "3.0.0"
},
"msg" : "you are logged in"
},
"session" : {
"id" : 162
},
"user" : {
"email" : "[email protected]",
"first_name" : "Unit",
"id" : 4096,
"last_name" : "Test",
"username" : "unit-test"
}
}
```

Now we're talking. But we're missing something. The point of sending `POST /session` is to establish a session we can use with subsequent requests. Let's also take a look at the HTTP response headers with the `-i` option to curl.

```
~ curl -i -X POST http://localhost:3000/session --header "Content-Type: application/json" -d @nt-auth.json
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
set-cookie: sid-nictool=Fe26.2**19f7d4f243faa77b048119b4a2bcbdcaa7826cdd853d8bdd3110f330ac6932c8*pzn_-OSy1SfoNpWbNvY3xw*RZQ8EgV2IGphwBz-Fb0AvBGofBwct-GnExEdxW-P-mtc1CWLuBJF0IyI7da_tMtp**07d92c1e89978b270fbdd449adcecbab3078b746c4167fe586f417be866c54d8*nDSOqzX79qmsztrHHjub7FgC7XiAxqGNdB-txLq8L84; Max-Age=3600; Expires=Sun, 25 Feb 2024 21:51:20 GMT; HttpOnly; SameSite=Strict; Path=/
content-length: 237
Date: Sun, 25 Feb 2024 20:51:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"user":{"id":4096,"first_name":"Unit","last_name":"Test","username":"unit-test","email":"[email protected]"},"group":{"id":4096,"name":"example.com"},"session":{"id":162},"meta":{"api":{"version":"3.0.0"},"msg":"you are logged in"}}
```

Notice the `set-cookie` header. We can add that cookie to each CLI request, making the requests very long, or save the cookie to a `cookie-jar` file, and then tell curl to sent that cookie with future requests:

```
curl --cookie-jar nt-session -X POST http://localhost:3000/session --header "Content-Type: application/json" -d @nt-auth.json
{"user":{"id":4096,"first_name":"Unit","last_name":"Test","username":"unit-test","email":"[email protected]"},"group":{"id":4096,"name":"example.com"},"session":{"id":162},"meta":{"api":{"version":"3.0.0"},"msg":"you are logged in"}}
```

and if we peek inside the cookie jar:

```sh
➜ ~ cat nt-session
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost FALSE / FALSE 1708898204 sid-nictool Fe26.2**7a4db1aa0d250c5ba5dda0560ef6cb2c33652f412ee385ebe022313f4fd206f1*g8kgix2HyZUvCKdc60ITMA*Pk3tlc4lYvDAs2J_ZyVHOhYyKWAsGZzbkMdHleLxNPQ55EDmO0vfZWTSILzhceQn**46883c6f21a76dddc10d7c1b0bc3a82302b989057bed459fe61f00eba7d7cacd*bBpV_eKE8VJEz-IDDobcI0nmJT54IndUmoWfE1Eu4fM
```

We can see that our session cookie has been saved. Now we can make other requests to the API using that session cookie:

```
curl -b nt-session -X GET http://localhost:3000/user/4096 --header "Content-Type: application/json" | json_pp
{
"group" : {
"id" : 4096
},
"meta" : {
"api" : {
"version" : "3.0.0"
},
"msg" : "here's your user"
},
"user" : {
"email" : "[email protected]",
"first_name" : "Unit",
"id" : 4096,
"last_name" : "Test",
"username" : "unit-test"
}
}
```

26 changes: 19 additions & 7 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
const fs = require('fs/promises')
import fs from 'node:fs/promises'
import fsSync from 'node:fs'

const YAML = require('yaml')
import YAML from 'yaml'

import { setEnv } from './util.js'
setEnv()

class Config {
constructor(opts = {}) {
this.cfg = {}
this.debug = process.env.NODE_DEBUG ? true : false
this.env = process.env.NODE_ENV ?? opts.env
this.getEnv(opts)
}

async getEnv(opts = {}) {
this.env = process.env.NODE_ENV ?? opts.env ?? ''
this.debug = Boolean(process.env.NODE_DEBUG)
if (this.debug) console.log(`debug: true, env: ${this.env}`)
}

async get(name, env) {
this.getEnv()

const cacheKey = [name, env ?? this.env].join(':')
if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached

Expand All @@ -23,10 +33,12 @@ class Config {
}

getSync(name, env) {
this.getEnv()

const cacheKey = [name, env ?? this.env].join(':')
if (this.cfg?.[cacheKey]) return this.cfg[cacheKey] // cached

const str = require('fs').readFileSync(`./conf.d/${name}.yml`, 'utf8')
const str = fsSync.readFileSync(`./conf.d/${name}.yml`, 'utf8')
const cfg = YAML.parse(str)

this.cfg[cacheKey] = applyDefaults(cfg[env ?? this.env], cfg.default)
Expand All @@ -36,7 +48,7 @@ class Config {

function applyDefaults(cfg = {}, defaults = {}) {
for (const d in defaults) {
if (cfg[d] === undefined) {
if ([undefined, null].includes(cfg[d])) {
cfg[d] = defaults[d]
} else if (typeof cfg[d] === 'object' && typeof defaults[d] === 'object') {
cfg[d] = applyDefaults(cfg[d], defaults[d])
Expand All @@ -45,4 +57,4 @@ function applyDefaults(cfg = {}, defaults = {}) {
return cfg
}

module.exports = new Config()
export default new Config()
51 changes: 32 additions & 19 deletions lib/config.test.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,59 @@
const assert = require('node:assert/strict')
const { describe, it } = require('node:test')
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'

const config = require('./config')
import Config from './config.js'

describe('config', function () {
describe('get', function () {
it(`loads mysql test config`, async function () {
const cfg = await config.get('mysql', 'test')
describe('config', () => {
describe('get', () => {
it(`loads mysql test config`, async () => {
const cfg = await Config.get('mysql', 'test')
assert.deepEqual(cfg, mysqlTestCfg)
})

it(`loads mysql test config syncronously`, function () {
const cfg = config.getSync('mysql', 'test')
it(`loads mysql test config syncronously`, () => {
const cfg = Config.getSync('mysql', 'test')
assert.deepEqual(cfg, mysqlTestCfg)
})

it(`loads mysql cov config`, async function () {
const cfg = await config.get('mysql', 'cov')
it(`loads mysql cov config`, async () => {
const cfg = await Config.get('mysql', 'cov')
assert.deepEqual(cfg, mysqlTestCfg)
})

it(`loads mysql cov config (from cache)`, async function () {
it(`loads mysql cov config (from cache)`, async () => {
process.env.NODE_DEBUG = 1
const cfg = await config.get('mysql', 'cov')
const cfg = await Config.get('mysql', 'cov')
assert.deepEqual(cfg, mysqlTestCfg)
process.env.NODE_DEBUG = ''
})

it(`loads session test config`, async function () {
const cfg = await config.get('session', 'test')
it(`loads session test config`, async () => {
const cfg = await Config.get('session', 'test')
assert.deepEqual(cfg, sessCfg)
})

it(`loads session test config syncronously`, function () {
const cfg = config.getSync('session', 'test')
it(`loads session test config syncronously`, () => {
const cfg = Config.getSync('session', 'test')
assert.deepEqual(cfg, sessCfg)
})

it(`loads http test config syncronously`, function () {
const cfg = config.getSync('http', 'test')
it(`loads http test config syncronously`, () => {
const cfg = Config.getSync('http', 'test')
assert.deepEqual(cfg, httpCfg)
})

it(`detects NODE_DEBUG env`, async () => {
process.env.NODE_DEBUG = 1
let cfg = await Config.get('mysql', 'test')
assert.equal(Config.debug, true)

process.env.NODE_DEBUG = ''
cfg = await Config.get('mysql', 'test')
assert.equal(Config.debug, false)

cfg = await Config.get('mysql', 'test')
assert.equal(cfg.user, 'root')
})
})
})

Expand Down
Loading

0 comments on commit 3a2dfab

Please sign in to comment.