ci: add pull request quality gates
CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s
CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s
Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
This commit was merged in pull request #12.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
name: Setup
|
||||
description: Set up the Node version required by this repository. Requires actions/checkout to have already run.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
@@ -0,0 +1,124 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
install-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- name: Clean install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Production build
|
||||
run: npm run build
|
||||
|
||||
- name: Archive dependencies
|
||||
run: tar -czf node_modules.tar.gz node_modules
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: node_modules-${{ github.run_id }}
|
||||
path: node_modules.tar.gz
|
||||
|
||||
- name: Archive build artifacts
|
||||
run: tar -czf build-artifacts.tar.gz dist
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts-${{ github.run_id }}
|
||||
path: build-artifacts.tar.gz
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- name: Clean install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- name: Clean install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check
|
||||
|
||||
typecheck-source:
|
||||
runs-on: ubuntu-latest
|
||||
needs: install-and-build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules-${{ github.run_id }}
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts-${{ github.run_id }}
|
||||
|
||||
- name: Restore dependencies and build artifacts
|
||||
run: |
|
||||
tar -xzf node_modules.tar.gz
|
||||
tar -xzf build-artifacts.tar.gz
|
||||
|
||||
- name: Typecheck source
|
||||
run: npm run typecheck
|
||||
|
||||
typecheck-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: install-and-build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules-${{ github.run_id }}
|
||||
|
||||
- name: Restore dependencies
|
||||
run: tar -xzf node_modules.tar.gz
|
||||
|
||||
- name: Typecheck tests
|
||||
run: npm run typecheck:test
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: install-and-build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.gitea/actions/setup
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: node_modules-${{ github.run_id }}
|
||||
|
||||
- name: Restore dependencies
|
||||
run: tar -xzf node_modules.tar.gz
|
||||
|
||||
- name: Run full test suite
|
||||
run: npm test
|
||||
@@ -0,0 +1 @@
|
||||
22
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
@@ -0,0 +1,22 @@
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**"],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"no-control-regex": "off",
|
||||
"no-empty": "off",
|
||||
"no-fallthrough": "off",
|
||||
"prefer-const": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
Generated
+1303
-20
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -25,7 +25,9 @@
|
||||
"test:shared": "node --import tsx/esm --import ./src/meta/dev/register.ts --test $(find test/backends -name 'index.test.ts' | sort)",
|
||||
"test:tools": "node --import tsx/esm --import ./src/meta/dev/register.ts --test $(find test/tools -name '*.test.ts' | sort)",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:test": "tsc --noEmit -p tsconfig.test.json"
|
||||
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
||||
"lint": "eslint .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/bodyparser": "^6.0.0",
|
||||
@@ -44,21 +46,25 @@
|
||||
"react-redux": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.5",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/koa": "^3.0.0",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/webpack-bundle-analyzer": "^4.7.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"dynohot": "^2.1.1",
|
||||
"eslint": "^9.39.5",
|
||||
"mini-css-extract-plugin": "^2.9.4",
|
||||
"prettier": "^3.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"scss-loader": "^0.0.1",
|
||||
"ts-loader": "^9.5.4",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"val-loader": "^6.0.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^6.0.1"
|
||||
|
||||
@@ -40,7 +40,7 @@ switch (cmd) {
|
||||
if (!current.length) console.log("(none)");
|
||||
for (const migration of current) {
|
||||
console.log(
|
||||
`- ${migration.id} (${migration.name}) applied at ${migration.applied_at}`
|
||||
`- ${migration.id} (${migration.name}) applied at ${migration.applied_at}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -73,7 +73,7 @@ switch (cmd) {
|
||||
password: await hashPassword("changeme"),
|
||||
});
|
||||
console.log(
|
||||
`Created user 'admin@codi.moe' (${uid}) with password 'changeme' and admin flag`
|
||||
`Created user 'admin@codi.moe' (${uid}) with password 'changeme' and admin flag`,
|
||||
);
|
||||
}
|
||||
users = await db.listUsers();
|
||||
@@ -88,7 +88,7 @@ switch (cmd) {
|
||||
expires_at: null,
|
||||
});
|
||||
console.log(
|
||||
`Created apikey '${token}' (${apikey.kid}) with permissions admin, all and no expiry`
|
||||
`Created apikey '${token}' (${apikey.kid}) with permissions admin, all and no expiry`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ if (args.length > 1) printUsage("too many arguments");
|
||||
|
||||
console.log(
|
||||
`Compiled with ${compiledSources.length} sources:`,
|
||||
compiledSources.join(", ")
|
||||
compiledSources.join(", "),
|
||||
);
|
||||
|
||||
const url = args[0] ?? "abode://";
|
||||
@@ -31,7 +31,7 @@ for (const source of sources) {
|
||||
console.log(`- ${source.name}`);
|
||||
console.log(
|
||||
" - protocols:",
|
||||
source.protocols.map((x) => `'${x}'`).join(" ")
|
||||
source.protocols.map((x) => `'${x}'`).join(" "),
|
||||
);
|
||||
const match = source.checkUrl(url);
|
||||
console.log(` - matches url: ${match}`);
|
||||
@@ -41,7 +41,7 @@ for (const source of sources) {
|
||||
console.log(
|
||||
` - generates an interface named ${db.name} ${
|
||||
db.backend ? "with" : "without"
|
||||
} backend`
|
||||
} backend`,
|
||||
);
|
||||
await db.close().catch(console.error);
|
||||
} catch (e) {
|
||||
@@ -53,7 +53,7 @@ for (const source of sources) {
|
||||
console.log(
|
||||
` - generates a migrator knowing ${
|
||||
db.listAvailableMigrations().length
|
||||
} migrations`
|
||||
} migrations`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -30,7 +30,7 @@ const bgColor = await new Promise<string>((ok, ko) => {
|
||||
process.stdin.once("data", (chunk) => {
|
||||
const result = chunk.toString("utf8");
|
||||
const match = result.match(
|
||||
/^\u001b]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)$/
|
||||
/^\u001b]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)$/,
|
||||
);
|
||||
if (!match) return ko("Didn't recognize terminal bg color");
|
||||
const [r, g, b] = match
|
||||
@@ -78,7 +78,7 @@ if (import.meta.hot) {
|
||||
|
||||
import.meta.hot.accept("../tui/App.js", (mod) => {
|
||||
fullscreenApp.instance.rerender(
|
||||
(mod.app as typeof app)({ db, bgColor, store })
|
||||
(mod.app as typeof app)({ db, bgColor, store }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ApiInterface implements DbInterface {
|
||||
}: {
|
||||
headers?: Record<string, string>;
|
||||
readonly?: boolean;
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
if (root.endsWith("/")) root = root.slice(0, -1);
|
||||
this.#root = root;
|
||||
@@ -82,7 +82,7 @@ export class ApiInterface implements DbInterface {
|
||||
params?: Record<string, string>;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const resolvedHeaders = { ...this.#headers, ...headers };
|
||||
if (body !== undefined) {
|
||||
@@ -247,7 +247,7 @@ export class ApiInterface implements DbInterface {
|
||||
});
|
||||
}
|
||||
async createApikey(
|
||||
apikey: CreateApikey
|
||||
apikey: CreateApikey,
|
||||
): Promise<[ClientApikey, `at_${string}`]> {
|
||||
this.#checkReadonly();
|
||||
const { apikey: key, token } = await this.#call<{
|
||||
|
||||
@@ -3,7 +3,7 @@ import { apiProtocols } from "./url.js";
|
||||
|
||||
const getApi = () =>
|
||||
import(/* webpackChunkName: 'dbsource-api' */ "./getdb.static.js").then(
|
||||
(x) => x.default
|
||||
(x) => x.default,
|
||||
);
|
||||
|
||||
const getApiDynamic: GetDbDynamic = {
|
||||
|
||||
+4
-2
@@ -17,7 +17,9 @@ export function parseApiUrl(url: string) {
|
||||
urlObj.href = urlObj.href.replace(/^abode\+/, "");
|
||||
const readonly = (urlObj.searchParams.get("readonly") ?? "0") !== "0";
|
||||
const headers = Object.fromEntries(
|
||||
[...urlObj.searchParams.entries()].filter(([param]) => param !== "readonly")
|
||||
[...urlObj.searchParams.entries()].filter(
|
||||
([param]) => param !== "readonly",
|
||||
),
|
||||
);
|
||||
if (urlObj.username) {
|
||||
headers["Authorization"] =
|
||||
@@ -26,7 +28,7 @@ export function parseApiUrl(url: string) {
|
||||
[
|
||||
decodeURIComponent(urlObj.username),
|
||||
decodeURIComponent(urlObj.password),
|
||||
].join(":")
|
||||
].join(":"),
|
||||
);
|
||||
urlObj.username = "";
|
||||
urlObj.password = "";
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getDbSources(url: string): Promise<GetDbStatic[]> {
|
||||
.catch(() => null)
|
||||
.then((dbSource) => {
|
||||
if (dbSource) dbSources.push(dbSource);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GetDbStatic } from "./types/GetDb.js";
|
||||
let rawGetDbSources: typeof getDbSources | undefined = undefined;
|
||||
const getGetDbSources = () =>
|
||||
import(/* webpackChunkName: 'dbsources' */ "./dbSources.static.js").then(
|
||||
(x) => x.getDbSources
|
||||
(x) => x.getDbSources,
|
||||
);
|
||||
|
||||
export async function getDbSources(url: string): Promise<GetDbStatic[]> {
|
||||
|
||||
@@ -92,7 +92,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
async getUserByEmail(email: string): Promise<ClientUser> {
|
||||
const user = await selectClientUser(
|
||||
this.#db,
|
||||
sql`u."email" = ${{ text: email }}`
|
||||
sql`u."email" = ${{ text: email }}`,
|
||||
);
|
||||
if (!user) throw new NotFoundAbodeError();
|
||||
return user;
|
||||
@@ -111,7 +111,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
SELECT "uid", "email", "name", "flags", "created_at", "updated_at", "password"
|
||||
FROM "users"
|
||||
WHERE "email" = ${{ text: email }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!rawUser) throw new NotFoundAbodeError();
|
||||
if (rawUser.password.startsWith("#")) throw new ConflictAbodeError();
|
||||
@@ -125,7 +125,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "users"
|
||||
WHERE "uid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
@@ -145,10 +145,10 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
${{ text: user.password }},
|
||||
${{ jsonb: user.flags }}
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getUserById(uid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateUser(user: UpdateUser): Promise<ClientUser> {
|
||||
@@ -161,7 +161,8 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
if ("name" in user && user.name !== undefined)
|
||||
updates.push(sql`"name" = ${{ text: user.name }}`);
|
||||
if ("password" in user && user.password !== undefined) {
|
||||
if (!isValidUserPassword(user.password)) throw new InvalidAbodeError();
|
||||
if (!isValidUserPassword(user.password))
|
||||
throw new InvalidAbodeError();
|
||||
if (user.password.startsWith("#")) {
|
||||
await tx.run(sql`
|
||||
DELETE FROM "apikeys"
|
||||
@@ -185,11 +186,11 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
"updated_at" = NOW(),
|
||||
${joinSql(updates, sql`, `)}
|
||||
WHERE "uid" = ${{ uuid: user.uid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getUserById(user.uid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "abodes"
|
||||
WHERE "aid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
@@ -228,10 +229,10 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
${{ uuid: ctx.uid }},
|
||||
${{ uuid: ctx.uid }}
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getAbodeById(aid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateAbode(abode: UpdateAbode, ctx: { uid: string }): Promise<Abode> {
|
||||
@@ -250,11 +251,11 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
"updated_by" = ${{ uuid: ctx.uid }},
|
||||
${joinSql(updates, sql`, `)}
|
||||
WHERE "aid" = ${{ uuid: abode.aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getAbodeById(abode.aid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,11 +271,11 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
async #getResidentById(
|
||||
uid: string,
|
||||
aid: string,
|
||||
db: WrappedPgClient
|
||||
db: WrappedPgClient,
|
||||
): Promise<Resident> {
|
||||
const resident = await selectResident(
|
||||
db,
|
||||
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`
|
||||
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`,
|
||||
);
|
||||
if (!resident) throw new NotFoundAbodeError();
|
||||
return resident;
|
||||
@@ -288,13 +289,13 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "residents"
|
||||
WHERE "uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
async createResident(
|
||||
resident: CreateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident> {
|
||||
this.#checkReadonly();
|
||||
return this.#db.rethrow(() =>
|
||||
@@ -309,15 +310,15 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
${{ uuid: ctx.uid }},
|
||||
${{ uuid: ctx.uid }}
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getResidentById(resident.uid, resident.aid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateResident(
|
||||
resident: updateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident> {
|
||||
this.#checkReadonly();
|
||||
const updates = calcUpdates({
|
||||
@@ -336,11 +337,11 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
WHERE
|
||||
"uid" = ${{ uuid: resident.uid }}
|
||||
AND "aid" = ${{ uuid: resident.aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getResidentById(resident.uid, resident.aid, tx);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -350,7 +351,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
sql`
|
||||
JOIN "residents" r ON u."uid" = r."uid"
|
||||
WHERE r."aid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
async listAbodesByUserId(id: string): Promise<Abode[]> {
|
||||
@@ -359,7 +360,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
sql`
|
||||
JOIN "residents" r ON a."aid" = r."aid"
|
||||
WHERE r."uid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,17 +419,17 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
|
||||
async #getApikeyByToken(
|
||||
token: `at_${string}`,
|
||||
db: WrappedPgClient
|
||||
db: WrappedPgClient,
|
||||
): Promise<ClientApikey> {
|
||||
const apikey = await selectClientApikey(
|
||||
db,
|
||||
sql`k."token" = ${{ text: token }}`
|
||||
sql`k."token" = ${{ text: token }}`,
|
||||
);
|
||||
if (!apikey) throw new NotFoundAbodeError();
|
||||
return apikey;
|
||||
}
|
||||
async getUserByApikey(
|
||||
token: `at_${string}`
|
||||
token: `at_${string}`,
|
||||
): Promise<[ClientUser, ClientApikey]> {
|
||||
const apikey = await this.#getApikeyByToken(token, this.#db);
|
||||
if (
|
||||
@@ -445,13 +446,13 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
async getApikeyById(kid: string): Promise<ClientApikey> {
|
||||
const apikey = await selectClientApikey(
|
||||
this.#db,
|
||||
sql`k."kid" = ${{ uuid: kid }}`
|
||||
sql`k."kid" = ${{ uuid: kid }}`,
|
||||
);
|
||||
if (!apikey) throw new NotFoundAbodeError();
|
||||
return apikey;
|
||||
}
|
||||
async createApikey(
|
||||
apikey: CreateApikey
|
||||
apikey: CreateApikey,
|
||||
): Promise<[ClientApikey, `at_${string}`]> {
|
||||
this.#checkReadonly();
|
||||
const token = createApikeyToken();
|
||||
@@ -459,7 +460,7 @@ export class PostgresInterface implements BackendDbInterface {
|
||||
let expires = apikey.expires_at;
|
||||
if (expires === undefined)
|
||||
expires = new Date(
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * 365
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * 365,
|
||||
).toISOString();
|
||||
if (expires && new Date(expires).getTime() < Date.now())
|
||||
throw new InvalidAbodeError();
|
||||
|
||||
@@ -24,7 +24,7 @@ export class PostgresMigrator implements Migrator {
|
||||
`SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = '_migrations'
|
||||
) AS "exists"`
|
||||
) AS "exists"`,
|
||||
);
|
||||
if (!existsResult.rows[0]?.exists) return null;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PostgresMigrator implements Migrator {
|
||||
name: string;
|
||||
applied_at: Date;
|
||||
}>(
|
||||
`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`
|
||||
`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`,
|
||||
);
|
||||
return result.rows.map((x) => ({
|
||||
...x,
|
||||
@@ -73,17 +73,16 @@ export class PostgresMigrator implements Migrator {
|
||||
throw new Error(`Applied migration ${id} (${name}) not known`);
|
||||
if (migration.name !== name)
|
||||
throw new Error(
|
||||
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`
|
||||
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`,
|
||||
);
|
||||
}
|
||||
|
||||
const start =
|
||||
migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1;
|
||||
const start = migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1;
|
||||
const end = migrations.indexOf(target) + 1;
|
||||
|
||||
if (end < start) {
|
||||
throw new Error(
|
||||
`Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${target.id}`
|
||||
`Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${target.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,7 @@ async function rethrow<R>(fn: () => Promise<R>): Promise<R> {
|
||||
|
||||
// Rollback on a broken connection can itself throw; the original error is
|
||||
// the one worth surfacing.
|
||||
export async function rollbackQuietly(
|
||||
client: pg.PoolClient
|
||||
): Promise<void> {
|
||||
export async function rollbackQuietly(client: pg.PoolClient): Promise<void> {
|
||||
try {
|
||||
await client.query("ROLLBACK");
|
||||
} catch {}
|
||||
@@ -58,7 +56,7 @@ abstract class WrappedPgBase implements WrappedPgClient {
|
||||
async all<R>(stmt: SqlCode): Promise<R[]> {
|
||||
const result = await this.#queryable.query(
|
||||
toPositional(stmt._sql),
|
||||
stmt._vars
|
||||
stmt._vars,
|
||||
);
|
||||
return result.rows as R[];
|
||||
}
|
||||
@@ -72,7 +70,7 @@ abstract class WrappedPgBase implements WrappedPgClient {
|
||||
async run(stmt: SqlCode): Promise<{ changes: number }> {
|
||||
const result = await this.#queryable.query(
|
||||
toPositional(stmt._sql),
|
||||
stmt._vars
|
||||
stmt._vars,
|
||||
);
|
||||
return { changes: result.rowCount ?? 0 };
|
||||
}
|
||||
|
||||
+14
-16
@@ -26,20 +26,18 @@ const sqlClientUser = sql`
|
||||
|
||||
export async function selectClientUser(
|
||||
db: WrappedPgClient,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): Promise<ClientUser | null> {
|
||||
const raw = await db.get<RawClientUser>(
|
||||
sql`${sqlClientUser} WHERE ${where}`
|
||||
);
|
||||
const raw = await db.get<RawClientUser>(sql`${sqlClientUser} WHERE ${where}`);
|
||||
if (raw) return pgToClientUser(raw);
|
||||
return null;
|
||||
}
|
||||
export async function selectClientUsers(
|
||||
db: WrappedPgClient,
|
||||
rest?: SqlCode
|
||||
rest?: SqlCode,
|
||||
): Promise<ClientUser[]> {
|
||||
const rows = await db.all<RawClientUser>(
|
||||
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser
|
||||
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser,
|
||||
);
|
||||
return rows.map(pgToClientUser);
|
||||
}
|
||||
@@ -59,7 +57,7 @@ const sqlAbode = sql`
|
||||
|
||||
export async function selectAbode(
|
||||
db: WrappedPgClient,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): Promise<Abode | null> {
|
||||
const raw = await db.get<RawAbode>(sql`${sqlAbode} WHERE ${where}`);
|
||||
if (raw) return pgToAbode(raw);
|
||||
@@ -67,10 +65,10 @@ export async function selectAbode(
|
||||
}
|
||||
export async function selectAbodes(
|
||||
db: WrappedPgClient,
|
||||
rest?: SqlCode
|
||||
rest?: SqlCode,
|
||||
): Promise<Abode[]> {
|
||||
const rows = await db.all<RawAbode>(
|
||||
rest ? sql`${sqlAbode} ${rest}` : sqlAbode
|
||||
rest ? sql`${sqlAbode} ${rest}` : sqlAbode,
|
||||
);
|
||||
return rows.map(pgToAbode);
|
||||
}
|
||||
@@ -91,7 +89,7 @@ const sqlResident = sql`
|
||||
|
||||
export async function selectResident(
|
||||
db: WrappedPgClient,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): Promise<Resident | null> {
|
||||
const raw = await db.get<RawResident>(sql`${sqlResident} WHERE ${where}`);
|
||||
if (raw) return pgToResident(raw);
|
||||
@@ -99,10 +97,10 @@ export async function selectResident(
|
||||
}
|
||||
export async function selectResidents(
|
||||
db: WrappedPgClient,
|
||||
where?: SqlCode
|
||||
where?: SqlCode,
|
||||
): Promise<Resident[]> {
|
||||
const rows = await db.all<RawResident>(
|
||||
where ? sql`${sqlResident} WHERE ${where}` : sqlResident
|
||||
where ? sql`${sqlResident} WHERE ${where}` : sqlResident,
|
||||
);
|
||||
return rows.map(pgToResident);
|
||||
}
|
||||
@@ -122,20 +120,20 @@ const sqlClientApikey = sql`
|
||||
|
||||
export async function selectClientApikey(
|
||||
db: WrappedPgClient,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): Promise<ClientApikey | null> {
|
||||
const raw = await db.get<RawClientApikey>(
|
||||
sql`${sqlClientApikey} WHERE ${where}`
|
||||
sql`${sqlClientApikey} WHERE ${where}`,
|
||||
);
|
||||
if (raw) return pgToClientApikey(raw);
|
||||
return null;
|
||||
}
|
||||
export async function selectClientApikeys(
|
||||
db: WrappedPgClient,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): Promise<ClientApikey[]> {
|
||||
const rows = await db.all<RawClientApikey>(
|
||||
sql`${sqlClientApikey} WHERE ${where}`
|
||||
sql`${sqlClientApikey} WHERE ${where}`,
|
||||
);
|
||||
return rows.map(pgToClientApikey);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function calcUpdates<T extends object>(updater: {
|
||||
for (const [prop, update] of Object.entries(updater)) {
|
||||
if (prop in obj) {
|
||||
updates.push(
|
||||
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!)
|
||||
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ export function isPgUrl(url: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePgUrl(url: string): { connectionString: string; readonly: boolean } {
|
||||
export function parsePgUrl(url: string): {
|
||||
connectionString: string;
|
||||
readonly: boolean;
|
||||
} {
|
||||
if (!isPgUrl(url)) throw new Error("Not a postgres: URL");
|
||||
const urlObj = new URL(url);
|
||||
const readonly = (urlObj.searchParams.get("readonly") ?? "0") !== "0";
|
||||
|
||||
@@ -110,7 +110,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
SELECT "uid", "email", "name", json("flags") AS "flags", "created_at", "updated_at", "password"
|
||||
FROM "users"
|
||||
WHERE "email" = ${{ text: email }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!rawUser) throw new NotFoundAbodeError();
|
||||
if (rawUser.password.startsWith("#")) throw new ConflictAbodeError();
|
||||
@@ -124,7 +124,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "users"
|
||||
WHERE "uid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
@@ -142,10 +142,10 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
${{ text: user.email }},
|
||||
${{ text: user.name }},
|
||||
${{ text: user.password }},${{ jsonb: user.flags }})
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getUserById(uid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateUser(user: UpdateUser): Promise<ClientUser> {
|
||||
@@ -179,11 +179,11 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
"updated_at" = datetime('now', 'localtime', 'subsec'),
|
||||
${joinSql(updates, sql`, `)}
|
||||
WHERE "uid" = ${{ uuid: user.uid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getUserById(user.uid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "abodes"
|
||||
WHERE "aid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
@@ -222,10 +222,10 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
${{ uuid: ctx.uid }},
|
||||
${{ uuid: ctx.uid }}
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getAbodeById(aid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateAbode(abode: UpdateAbode, ctx: { uid: string }): Promise<Abode> {
|
||||
@@ -244,11 +244,11 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
"updated_by" = ${{ uuid: ctx.uid }},
|
||||
${joinSql(updates, sql`, `)}
|
||||
WHERE "aid" = ${{ uuid: abode.aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getAbodeById(abode.aid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
#getResidentById(uid: string, aid: string): Resident {
|
||||
const resident = selectResident(
|
||||
this.#db,
|
||||
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`
|
||||
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`,
|
||||
);
|
||||
if (!resident) throw new NotFoundAbodeError();
|
||||
return resident;
|
||||
@@ -278,13 +278,13 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
sql`
|
||||
DELETE FROM "residents"
|
||||
WHERE "uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
async createResident(
|
||||
resident: CreateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident> {
|
||||
this.#checkReadonly();
|
||||
return this.#db.rethrow(() =>
|
||||
@@ -299,15 +299,15 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
${{ uuid: ctx.uid }},
|
||||
${{ uuid: ctx.uid }}
|
||||
)
|
||||
`
|
||||
`,
|
||||
);
|
||||
return this.#getResidentById(resident.uid, resident.aid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateResident(
|
||||
resident: updateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident> {
|
||||
this.#checkReadonly();
|
||||
const updates = calcUpdates({
|
||||
@@ -326,11 +326,11 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
WHERE
|
||||
"uid" = ${{ uuid: resident.uid }}
|
||||
AND "aid" = ${{ uuid: resident.aid }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getResidentById(resident.uid, resident.aid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
sql`
|
||||
JOIN "residents" r ON u."uid" = r."uid"
|
||||
WHERE r."aid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
async listAbodesByUserId(id: string): Promise<Abode[]> {
|
||||
@@ -349,7 +349,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
sql`
|
||||
JOIN "residents" r ON a."aid" = r."aid"
|
||||
WHERE r."uid" = ${{ uuid: id }}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -405,13 +405,13 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
#getApikeyByToken(token: `at_${string}`): ClientApikey {
|
||||
const apikey = selectClientApikey(
|
||||
this.#db,
|
||||
sql`"token" = ${{ text: token }}`
|
||||
sql`"token" = ${{ text: token }}`,
|
||||
);
|
||||
if (!apikey) throw new NotFoundAbodeError();
|
||||
return apikey;
|
||||
}
|
||||
async getUserByApikey(
|
||||
token: `at_${string}`
|
||||
token: `at_${string}`,
|
||||
): Promise<[ClientUser, ClientApikey]> {
|
||||
const apikey = this.#getApikeyByToken(token);
|
||||
if (
|
||||
@@ -431,7 +431,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
return apikey;
|
||||
}
|
||||
async createApikey(
|
||||
apikey: CreateApikey
|
||||
apikey: CreateApikey,
|
||||
): Promise<[ClientApikey, `at_${string}`]> {
|
||||
this.#checkReadonly();
|
||||
const token = createApikeyToken();
|
||||
@@ -439,7 +439,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
let expires = apikey.expires_at;
|
||||
if (expires === undefined)
|
||||
expires = new Date(
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * 365
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * 365,
|
||||
).toISOString();
|
||||
if (expires && new Date(expires).getTime() < Date.now())
|
||||
throw new InvalidAbodeError();
|
||||
@@ -480,7 +480,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
async deleteNoteById(nid: string): Promise<void> {
|
||||
this.#checkReadonly();
|
||||
const { changes } = this.#db.run(
|
||||
sql`DELETE FROM "notes" WHERE "nid" = ${{ uuid: nid }}`
|
||||
sql`DELETE FROM "notes" WHERE "nid" = ${{ uuid: nid }}`,
|
||||
);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
}
|
||||
@@ -502,7 +502,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
)
|
||||
`);
|
||||
return this.#getNoteById(nid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async updateNote(note: UpdateNote, ctx: { uid: string }): Promise<Note> {
|
||||
@@ -525,7 +525,7 @@ export class SqliteInterface implements BackendDbInterface {
|
||||
`);
|
||||
if (!changes) throw new NotFoundAbodeError();
|
||||
return this.#getNoteById(note.nid);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
async listNotesByAbodeId(aid: string): Promise<PartialNote[]> {
|
||||
|
||||
@@ -16,12 +16,11 @@ export class SqliteMigrator implements Migrator {
|
||||
}
|
||||
|
||||
#listAppliedMigrations():
|
||||
| { id: number; name: string; applied_at: string }[]
|
||||
| null {
|
||||
{ id: number; name: string; applied_at: string }[] | null {
|
||||
try {
|
||||
return this.#db
|
||||
.all<{ id: number; name: string; applied_at: string }>(
|
||||
sql`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`
|
||||
sql`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`,
|
||||
)
|
||||
.map((x) => ({ ...x, applied_at: sqliteToDate(x.applied_at) }));
|
||||
} catch (e) {
|
||||
@@ -59,7 +58,7 @@ export class SqliteMigrator implements Migrator {
|
||||
throw new Error(`Applied migration ${id} (${name}) not known`);
|
||||
if (migration.name !== name)
|
||||
throw new Error(
|
||||
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`
|
||||
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ export class SqliteMigrator implements Migrator {
|
||||
throw new Error(
|
||||
`Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${
|
||||
target.id
|
||||
}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ export class SqliteMigrator implements Migrator {
|
||||
sql`
|
||||
INSERT INTO "_migrations"("id", "name")
|
||||
VALUES (${{ int: migration.id }}, ${{ text: migration.name }})
|
||||
`
|
||||
`,
|
||||
);
|
||||
this.#db.run(sql`COMMIT`);
|
||||
} catch (e) {
|
||||
|
||||
@@ -22,7 +22,7 @@ export function uuidToSqlite(uuid: string) {
|
||||
}
|
||||
export function sqliteToUuid(uuid: Buffer | Uint8Array) {
|
||||
const hex = (uuid instanceof Buffer ? uuid : Buffer.from(uuid)).toString(
|
||||
"hex"
|
||||
"hex",
|
||||
);
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
@@ -126,7 +126,7 @@ export function sqliteToResident(resident: {
|
||||
|
||||
const defaultApikeyPermissions: ApikeyPermissions = {};
|
||||
export function sqliteToApikeyPermissions(
|
||||
permissions: string
|
||||
permissions: string,
|
||||
): ApikeyPermissions {
|
||||
const parsed = JSON.parse(permissions);
|
||||
const out = { ...defaultApikeyPermissions };
|
||||
@@ -184,7 +184,7 @@ export function sqliteToNoteProperties(props: string): NoteProperties {
|
||||
}
|
||||
|
||||
export function sqliteToPartialNoteProperties(
|
||||
props: string
|
||||
props: string,
|
||||
): PartialNoteProperties {
|
||||
const base = sqliteToNoteProperties(props);
|
||||
return { type: base.type ?? "note" };
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sqliteProtocols } from "./url.js";
|
||||
|
||||
const getSqlite = () =>
|
||||
import(/* webpackChunkName: 'dbsource-sqlite' */ "./getdb.static.js").then(
|
||||
(x) => x.default
|
||||
(x) => x.default,
|
||||
);
|
||||
|
||||
const getSqliteDynamic: GetDbDynamic = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { WrappedDb, WrappedDbOptions } from "./types.js";
|
||||
|
||||
function getDatabase(
|
||||
path: string,
|
||||
options?: Omit<sqlite.Options, "nativeBinding">
|
||||
options?: Omit<sqlite.Options, "nativeBinding">,
|
||||
): sqlite.Database {
|
||||
if (!natives.sqlite) throw new Error("No natives found for better-sqlite3");
|
||||
options = { ...options };
|
||||
|
||||
@@ -4,7 +4,7 @@ import { node, bs3 } from "./implementations.js";
|
||||
export function getWrappedDb(
|
||||
kind: "any" | "node" | "bs3",
|
||||
path: string,
|
||||
options: WrappedDbOptions
|
||||
options: WrappedDbOptions,
|
||||
): WrappedDb {
|
||||
if (kind === "node") {
|
||||
if (!node) throw new Error("Requesting unavailable node backend");
|
||||
|
||||
+13
-11
@@ -29,7 +29,7 @@ const sqlClientUser = sql`
|
||||
|
||||
export function selectClientUser(
|
||||
db: WrappedDb,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): ClientUser | null {
|
||||
const rawUser = db.get<RawClientUser>(sql`${sqlClientUser} WHERE ${where}`);
|
||||
if (rawUser) return sqliteToClientUser(rawUser);
|
||||
@@ -37,7 +37,7 @@ export function selectClientUser(
|
||||
}
|
||||
export function selectClientUsers(db: WrappedDb, rest?: SqlCode): ClientUser[] {
|
||||
const rawUsers = db.all<RawClientUser>(
|
||||
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser
|
||||
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser,
|
||||
);
|
||||
return rawUsers.map(sqliteToClientUser);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function selectAbode(db: WrappedDb, where: SqlCode): Abode | null {
|
||||
}
|
||||
export function selectAbodes(db: WrappedDb, rest?: SqlCode): Abode[] {
|
||||
const rawAbodes = db.all<RawAbode>(
|
||||
rest ? sql`${sqlAbode} ${rest}` : sqlAbode
|
||||
rest ? sql`${sqlAbode} ${rest}` : sqlAbode,
|
||||
);
|
||||
return rawAbodes.map(sqliteToAbode);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export function selectResident(db: WrappedDb, where: SqlCode): Resident | null {
|
||||
}
|
||||
export function selectResidents(db: WrappedDb, where?: SqlCode): Resident[] {
|
||||
const rawResidents = db.all<RawResident>(
|
||||
where ? sql`${sqlResident} WHERE ${where}` : sqlResident
|
||||
where ? sql`${sqlResident} WHERE ${where}` : sqlResident,
|
||||
);
|
||||
return rawResidents.map(sqliteToResident);
|
||||
}
|
||||
@@ -108,20 +108,20 @@ const sqlClientApikey = sql`
|
||||
|
||||
export function selectClientApikey(
|
||||
db: WrappedDb,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): ClientApikey | null {
|
||||
const rawApikey = db.get<RawClientApikey>(
|
||||
sql`${sqlClientApikey} WHERE ${where}`
|
||||
sql`${sqlClientApikey} WHERE ${where}`,
|
||||
);
|
||||
if (rawApikey) return sqliteToClientApikey(rawApikey);
|
||||
return null;
|
||||
}
|
||||
export function selectClientApikeys(
|
||||
db: WrappedDb,
|
||||
where: SqlCode
|
||||
where: SqlCode,
|
||||
): ClientApikey[] {
|
||||
const rawApikeys = db.all<RawClientApikey>(
|
||||
sql`${sqlClientApikey} WHERE ${where}`
|
||||
sql`${sqlClientApikey} WHERE ${where}`,
|
||||
);
|
||||
return rawApikeys.map(sqliteToClientApikey);
|
||||
}
|
||||
@@ -156,15 +156,17 @@ export function selectNote(db: WrappedDb, where: SqlCode): Note | null {
|
||||
return null;
|
||||
}
|
||||
export function selectNotes(db: WrappedDb, where?: SqlCode): Note[] {
|
||||
const raws = db.all<RawNote>(where ? sql`${sqlNote} WHERE ${where}` : sqlNote);
|
||||
const raws = db.all<RawNote>(
|
||||
where ? sql`${sqlNote} WHERE ${where}` : sqlNote,
|
||||
);
|
||||
return raws.map(sqliteToNote);
|
||||
}
|
||||
export function selectPartialNotes(
|
||||
db: WrappedDb,
|
||||
where?: SqlCode
|
||||
where?: SqlCode,
|
||||
): PartialNote[] {
|
||||
const raws = db.all<RawPartialNote>(
|
||||
where ? sql`${sqlPartialNote} WHERE ${where}` : sqlPartialNote
|
||||
where ? sql`${sqlPartialNote} WHERE ${where}` : sqlPartialNote,
|
||||
);
|
||||
return raws.map(sqliteToPartialNote);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function calcUpdates<T extends object>(updater: {
|
||||
for (const [prop, update] of Object.entries(updater)) {
|
||||
if (prop in obj) {
|
||||
updates.push(
|
||||
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!)
|
||||
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function isSqliteUrl(url: string) {
|
||||
}
|
||||
|
||||
export function parseSqliteUrl(
|
||||
url: string
|
||||
url: string,
|
||||
): ["any" | "node" | "bs3", string, WrappedDbOptions] {
|
||||
if (!isSqliteUrl(url)) throw new Error("Not sqlite: protocol");
|
||||
const urlObj = new URL(url);
|
||||
|
||||
@@ -40,11 +40,11 @@ export interface DbInterface {
|
||||
deleteResidentById(uid: string, aid: string): Promise<void>;
|
||||
createResident(
|
||||
resident: CreateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident>;
|
||||
updateResident(
|
||||
resident: updateResident,
|
||||
ctx: { uid: string }
|
||||
ctx: { uid: string },
|
||||
): Promise<Resident>;
|
||||
|
||||
// list residents by member
|
||||
@@ -104,7 +104,7 @@ export function isBackendInterface(db: DbInterface): db is BackendDbInterface {
|
||||
] as const
|
||||
).every(
|
||||
(x) =>
|
||||
x in db && typeof (db as Partial<BackendDbInterface>)[x] === "function"
|
||||
x in db && typeof (db as Partial<BackendDbInterface>)[x] === "function",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export type LoginUser = {
|
||||
};
|
||||
|
||||
export function isValidUserPassword(
|
||||
password: string
|
||||
password: string,
|
||||
): password is User["password"] {
|
||||
if (password.startsWith("#")) {
|
||||
return ["unset"].includes(password.slice(1));
|
||||
|
||||
@@ -2,7 +2,7 @@ if (import.meta.hot) {
|
||||
import.meta.hot.on("message", (msg) => {
|
||||
if (
|
||||
msg.includes(
|
||||
"A pending update was not accepted, and reached the root module:"
|
||||
"A pending update was not accepted, and reached the root module:",
|
||||
)
|
||||
) {
|
||||
throw new Error("[hot] Restarting due to unaccepted pending update");
|
||||
|
||||
@@ -32,23 +32,23 @@ async function getPackageJsonDir(path: string): Promise<string | null> {
|
||||
|
||||
export async function findNative(
|
||||
module: string,
|
||||
native: string
|
||||
native: string,
|
||||
): Promise<string> {
|
||||
const path = await getPackageJsonDir(
|
||||
createRequire(import.meta.url).resolve(module)
|
||||
createRequire(import.meta.url).resolve(module),
|
||||
);
|
||||
if (!path) throw new Error(`Cannot find module directory for ${module}`);
|
||||
const file = await find(path, native);
|
||||
if (!file)
|
||||
throw new Error(
|
||||
`Cannot find native ${native} of package ${module} in ${path}`
|
||||
`Cannot find native ${native} of package ${module} in ${path}`,
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function tryFindNative(
|
||||
module: string,
|
||||
native: string
|
||||
native: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
return await findNative(module, native);
|
||||
|
||||
@@ -22,8 +22,8 @@ export async function webpack(): Promise<{ code: string }> {
|
||||
let code: string = standaloneCode(
|
||||
validator,
|
||||
Object.fromEntries(
|
||||
Object.entries(schemas).map(([id, schema]) => [id, schema.$id])
|
||||
)
|
||||
Object.entries(schemas).map(([id, schema]) => [id, schema.$id]),
|
||||
),
|
||||
);
|
||||
|
||||
// assign the .schema ourselves to the validation functions
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface PopupManagerContextData {
|
||||
openPopup(popup: ComponentType<{ id: string; onClose: () => void }>): string;
|
||||
openPopup<T>(
|
||||
popup: ComponentType<{ id: string; onClose: () => void } & T>,
|
||||
props: T
|
||||
props: T,
|
||||
): string;
|
||||
|
||||
closePopup(id: string): void;
|
||||
@@ -34,7 +34,7 @@ export function PopupManager({ children }: { children: ReactNode }) {
|
||||
const openPopup = useCallback<PopupManagerContextData["openPopup"]>(
|
||||
(
|
||||
Component: ComponentType<{ id: string; onClose: () => void }>,
|
||||
props = {}
|
||||
props = {},
|
||||
) => {
|
||||
const id = crypto.randomUUID();
|
||||
Object.assign(props, {
|
||||
@@ -45,14 +45,14 @@ export function PopupManager({ children }: { children: ReactNode }) {
|
||||
setPopups((prev) => [...prev, { id, Component, props }]);
|
||||
return id;
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
const closePopup = useCallback((id: string) => {
|
||||
setPopups((prev) => prev.filter((x) => x.id !== id));
|
||||
}, []);
|
||||
const ctx = useMemo<PopupManagerContextData>(
|
||||
() => ({ openPopup, closePopup }),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useDataResidentsByAbodeId(aid: string) {
|
||||
const status = useLoad(loadResidentsByAbodeId, { aid });
|
||||
const residents = useMemo(
|
||||
() => Object.values(allResidents).filter((x) => x.aid === aid),
|
||||
[allResidents, aid]
|
||||
[allResidents, aid],
|
||||
);
|
||||
return { ...status, residents };
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function useDataResidentsByUserId(uid: string) {
|
||||
const status = useLoad(loadResidentsByUserId, { uid });
|
||||
const residents = useMemo(
|
||||
() => Object.values(allResidents).filter((x) => x.uid === uid),
|
||||
[allResidents, uid]
|
||||
[allResidents, uid],
|
||||
);
|
||||
return { ...status, residents };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Store } from "../store/store.js";
|
||||
import { useStore } from "../store/react.js";
|
||||
|
||||
export function useAction<P extends any[], R>(
|
||||
action: (...params: [...P, { db: DbInterface; store: Store }]) => Promise<R>
|
||||
action: (...params: [...P, { db: DbInterface; store: Store }]) => Promise<R>,
|
||||
): (...args: P) => Promise<R> {
|
||||
const db = use(DbContext);
|
||||
const store = useStore();
|
||||
@@ -15,6 +15,6 @@ export function useAction<P extends any[], R>(
|
||||
if (!db) throw new Error("DB not present");
|
||||
return action(...params, { db, store });
|
||||
},
|
||||
[action, db]
|
||||
[action, db],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useLoad<P>(loader: Loader<P>, params?: P): UseLoadResult {
|
||||
|
||||
const refresh = useCallback(
|
||||
() => load({ loader, params: params!, store, db, refresh: true }),
|
||||
[loader, params, db]
|
||||
[loader, params, db],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Store } from "../store.js";
|
||||
|
||||
export async function deleteAbodeById(
|
||||
aid: string,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
waitForLoadIfLoading(store, "loadAllAbodes"),
|
||||
@@ -22,7 +22,7 @@ export async function deleteAbodeById(
|
||||
|
||||
export async function updateAbode(
|
||||
abode: UpdateAbode,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
waitForLoadIfLoading(store, "loadAllAbodes"),
|
||||
@@ -38,7 +38,7 @@ export async function updateAbode(
|
||||
|
||||
export async function createAbode(
|
||||
abode: CreateAbode,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<string> {
|
||||
await waitForLoadIfLoading(store, "loadAllAbodes");
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Store } from "../store.js";
|
||||
|
||||
export async function deleteUserById(
|
||||
uid: string,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
waitForLoadIfLoading(store, "loadAllUsers"),
|
||||
@@ -24,7 +24,7 @@ export async function deleteUserById(
|
||||
|
||||
export async function updateUser(
|
||||
user: UpdateUser,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
waitForLoadIfLoading(store, "loadAllUsers"),
|
||||
@@ -41,7 +41,7 @@ export async function updateUser(
|
||||
|
||||
export async function createUser(
|
||||
user: CreateUser,
|
||||
{ store, db }: { store: Store; db: DbInterface }
|
||||
{ store, db }: { store: Store; db: DbInterface },
|
||||
): Promise<string> {
|
||||
await waitForLoadIfLoading(store, "loadAllUsers");
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ async function loadImpl<P>({
|
||||
setLoading([
|
||||
id,
|
||||
{ status: refresh ? "refreshing" : "loading", type, params },
|
||||
])
|
||||
]),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
@@ -69,7 +69,10 @@ async function loadImpl<P>({
|
||||
store.dispatch(setLoading([id, { status: "loaded", type, params }]));
|
||||
} catch (e) {
|
||||
store.dispatch(
|
||||
setLoading([id, { status: "error", type, params, error: objectError(e) }])
|
||||
setLoading([
|
||||
id,
|
||||
{ status: "error", type, params, error: objectError(e) },
|
||||
]),
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
@@ -114,7 +117,7 @@ export function loader<P>(loader: Loader<P>): Loader<P> {
|
||||
export async function waitForLoadIfLoading(
|
||||
store: Store,
|
||||
id: string,
|
||||
{ signal }: { signal?: AbortSignal } = {}
|
||||
{ signal }: { signal?: AbortSignal } = {},
|
||||
) {
|
||||
if (!getLoadingStatus(store.getState(), id)) return;
|
||||
return waitFor(
|
||||
@@ -123,6 +126,6 @@ export async function waitForLoadIfLoading(
|
||||
const status = getLoadingStatus(state, id);
|
||||
return status === "loaded" || status === "error";
|
||||
},
|
||||
{ signal }
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Provider(
|
||||
props: Omit<ProviderProps, "context" | "store" | "serverState"> & {
|
||||
store: Store;
|
||||
serverState?: State;
|
||||
}
|
||||
},
|
||||
) {
|
||||
return <RawProvider context={AbodeStoreContext} {...props} />;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const loadingSlice = createSlice({
|
||||
reducers: {
|
||||
setLoading: (
|
||||
state,
|
||||
action: PayloadAction<[id: string, state: LoadingState]>
|
||||
action: PayloadAction<[id: string, state: LoadingState]>,
|
||||
) => {
|
||||
state[action.payload[0]] = action.payload[1];
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ const usersSlice = createSlice({
|
||||
getUser: usersSelectors.selectById,
|
||||
getUserByEmail: (state, email: string) =>
|
||||
Object.values(state.entities).find(
|
||||
(x): x is ClientUser => "email" in x && x.email === email
|
||||
(x): x is ClientUser => "email" in x && x.email === email,
|
||||
),
|
||||
getUsers: usersSelectors.selectEntities,
|
||||
getUserIds: usersSelectors.selectIds,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { State, Store } from "./store.js";
|
||||
export async function waitFor(
|
||||
store: Store,
|
||||
cond: (state: State) => boolean,
|
||||
{ signal }: { signal?: AbortSignal } = {}
|
||||
{ signal }: { signal?: AbortSignal } = {},
|
||||
): Promise<void> {
|
||||
return new Promise<void>((ok, ko) => {
|
||||
signal?.throwIfAborted();
|
||||
@@ -28,7 +28,7 @@ export async function waitFor(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
{ signal: controller.signal }
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+19
-19
@@ -1,23 +1,23 @@
|
||||
export { default as user } from "./user/user.schema.json" with {type: 'json'};
|
||||
export { default as createuser } from "./user/createuser.schema.json" with {type: 'json'};
|
||||
export { default as updateuser } from "./user/updateuser.schema.json" with {type: 'json'};
|
||||
export { default as partialuser } from "./user/partialuser.schema.json" with {type: 'json'};
|
||||
export { default as clientuser } from "./user/clientuser.schema.json" with {type: 'json'};
|
||||
export { default as userflags } from "./user/userflags.schema.json" with {type: 'json'};
|
||||
export { default as loginuser } from "./user/loginuser.schema.json" with {type: 'json'};
|
||||
export { default as user } from "./user/user.schema.json" with { type: "json" };
|
||||
export { default as createuser } from "./user/createuser.schema.json" with { type: "json" };
|
||||
export { default as updateuser } from "./user/updateuser.schema.json" with { type: "json" };
|
||||
export { default as partialuser } from "./user/partialuser.schema.json" with { type: "json" };
|
||||
export { default as clientuser } from "./user/clientuser.schema.json" with { type: "json" };
|
||||
export { default as userflags } from "./user/userflags.schema.json" with { type: "json" };
|
||||
export { default as loginuser } from "./user/loginuser.schema.json" with { type: "json" };
|
||||
|
||||
export { default as abode } from './abode/abode.schema.json' with {type: 'json'};
|
||||
export { default as createabode } from './abode/createabode.schema.json' with {type: 'json'};
|
||||
export { default as updateabode } from './abode/updateabode.schema.json' with {type: 'json'};
|
||||
export { default as abode } from "./abode/abode.schema.json" with { type: "json" };
|
||||
export { default as createabode } from "./abode/createabode.schema.json" with { type: "json" };
|
||||
export { default as updateabode } from "./abode/updateabode.schema.json" with { type: "json" };
|
||||
|
||||
export { default as resident } from './resident/resident.schema.json' with {type: 'json'};
|
||||
export { default as createresident } from './resident/createresident.schema.json' with {type: 'json'};
|
||||
export { default as updateresident } from './resident/updateresident.schema.json' with {type: 'json'};
|
||||
export { default as residentflags } from './resident/residentflags.schema.json' with {type: 'json'};
|
||||
export { default as resident } from "./resident/resident.schema.json" with { type: "json" };
|
||||
export { default as createresident } from "./resident/createresident.schema.json" with { type: "json" };
|
||||
export { default as updateresident } from "./resident/updateresident.schema.json" with { type: "json" };
|
||||
export { default as residentflags } from "./resident/residentflags.schema.json" with { type: "json" };
|
||||
|
||||
export { default as createapikey } from './apikey/createapikey.schema.json' with {type: 'json'};
|
||||
export { default as apikeypermissions } from './apikey/apikeypermissions.schema.json' with {type: 'json'};
|
||||
export { default as createapikey } from "./apikey/createapikey.schema.json" with { type: "json" };
|
||||
export { default as apikeypermissions } from "./apikey/apikeypermissions.schema.json" with { type: "json" };
|
||||
|
||||
export { default as createnote } from './note/createnote.schema.json' with {type: 'json'};
|
||||
export { default as updatenote } from './note/updatenote.schema.json' with {type: 'json'};
|
||||
export { default as partialnoteproperties } from './note/partialnoteproperties.schema.json' with {type: 'json'};
|
||||
export { default as createnote } from "./note/createnote.schema.json" with { type: "json" };
|
||||
export { default as updatenote } from "./note/updatenote.schema.json" with { type: "json" };
|
||||
export { default as partialnoteproperties } from "./note/partialnoteproperties.schema.json" with { type: "json" };
|
||||
|
||||
@@ -39,7 +39,7 @@ function checkSchema(name: string, schema: AnySchema) {
|
||||
if (!schema.$id) throw new Error(`Missing $id for schema ${name}`);
|
||||
if (
|
||||
!schema.$id.match(
|
||||
/^https:\/\/abode\.codi\.moe\/schema\/[a-zA-Z0_9_-]+\.schema\.json$/
|
||||
/^https:\/\/abode\.codi\.moe\/schema\/[a-zA-Z0_9_-]+\.schema\.json$/,
|
||||
)
|
||||
)
|
||||
throw new Error(`Unexpected $id for schema ${name}`);
|
||||
|
||||
@@ -18,7 +18,7 @@ const validators = Object.fromEntries(
|
||||
Object.entries(schemas).map(([name, schema]) => [
|
||||
name,
|
||||
validator.compile(schema),
|
||||
])
|
||||
]),
|
||||
) as unknown as {
|
||||
[T in keyof Types]: {
|
||||
(obj: unknown): obj is Types[T];
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ function App() {
|
||||
(input, key) => {
|
||||
if (input === "q" || key.escape) app.exit();
|
||||
},
|
||||
{ isActive }
|
||||
{ isActive },
|
||||
);
|
||||
|
||||
const [activeCollection, setActiveCollection] =
|
||||
|
||||
@@ -41,11 +41,11 @@ export function AbodesPanel() {
|
||||
|
||||
const onSelect = useCallback(
|
||||
(abode: Abode) => openPopup(AbodePopup, { aid: abode.aid }),
|
||||
[openPopup]
|
||||
[openPopup],
|
||||
);
|
||||
const buttons = useMemo<ButtonListItem[]>(
|
||||
() => [{ children: "New", onClick: () => openPopup(CreateAbodePopup) }],
|
||||
[openPopup]
|
||||
[openPopup],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,11 +59,11 @@ export function UsersPanel() {
|
||||
|
||||
const onSelect = useCallback(
|
||||
(user: ClientUser | PartialUser) => openPopup(UserPopup, { uid: user.uid }),
|
||||
[openPopup]
|
||||
[openPopup],
|
||||
);
|
||||
const buttons = useMemo<ButtonListItem[]>(
|
||||
() => [{ children: "New", onClick: () => openPopup(CreateUserPopup) }],
|
||||
[openPopup]
|
||||
[openPopup],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ export function Button({
|
||||
(input, key) => {
|
||||
if (input === " " || key.return) onClick();
|
||||
},
|
||||
{ isActive: isFocused }
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
return <Text inverse={isFocused}>[{children}]</Text>;
|
||||
@@ -66,7 +66,7 @@ export function ButtonList({
|
||||
setSelected((prev) => (prev - 1 + buttons.length) % buttons.length);
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused || forceFocus || false }
|
||||
{ isActive: isFocused || forceFocus || false },
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ListBox<T extends string>({
|
||||
setSelected(items[(items.indexOf(selected) + 1) % items.length]);
|
||||
} else if (key.upArrow) {
|
||||
setSelected(
|
||||
items[(items.indexOf(selected) - 1 + items.length) % items.length]
|
||||
items[(items.indexOf(selected) - 1 + items.length) % items.length],
|
||||
);
|
||||
} else if (key.pageUp) {
|
||||
setSelected(items[0]);
|
||||
@@ -57,7 +57,7 @@ export function ListBox<T extends string>({
|
||||
setSelected(items[items.length - 1]);
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused }
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,14 +36,14 @@ export function ListDisplay<T>({
|
||||
onSelect?.(items[selected]);
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused }
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected < start) setStart(selected);
|
||||
else if (selected >= slice)
|
||||
setStart(
|
||||
Math.max(Math.min(selected - slice + start + 1, items.length - 1), 0)
|
||||
Math.max(Math.min(selected - slice + start + 1, items.length - 1), 0),
|
||||
);
|
||||
}, [selected, start, slice, items.length]);
|
||||
useEffect(() => {
|
||||
@@ -56,7 +56,7 @@ export function ListDisplay<T>({
|
||||
|
||||
const indexed = useMemo(
|
||||
() => items.map((item, index) => ({ item, index })),
|
||||
[items]
|
||||
[items],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Popup({
|
||||
(_, key) => {
|
||||
if (key.escape) onClose?.();
|
||||
},
|
||||
{ isActive: active && !!onClose }
|
||||
{ isActive: active && !!onClose },
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,7 +52,7 @@ export function SearchPanel<T>({
|
||||
refresh?.();
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused && !!refresh }
|
||||
{ isActive: isFocused && !!refresh },
|
||||
);
|
||||
|
||||
const topbar = !!match || !!buttons?.length;
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@ import { argon2id, argon2Verify } from "hash-wasm";
|
||||
|
||||
export async function validatePassword(
|
||||
password: string,
|
||||
hash: string
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
return await argon2Verify({ password, hash });
|
||||
}
|
||||
|
||||
export async function hashPassword(
|
||||
password: string
|
||||
password: string,
|
||||
): Promise<`$${string}$${string}`> {
|
||||
const salt = new Uint8Array(16);
|
||||
crypto.getRandomValues(salt);
|
||||
|
||||
+12
-14
@@ -7,11 +7,11 @@ const escapes = {
|
||||
};
|
||||
|
||||
function parseAdd<T>(
|
||||
rest: (Record<string, string> | string | ((writer: XmlWriter) => T))[]
|
||||
rest: (Record<string, string> | string | ((writer: XmlWriter) => T))[],
|
||||
): [
|
||||
props?: Record<string, string>,
|
||||
content?: string,
|
||||
children?: (writer: XmlWriter) => T
|
||||
children?: (writer: XmlWriter) => T,
|
||||
] {
|
||||
let props: Record<string, string> | undefined;
|
||||
let children: ((writer: XmlWriter) => T) | undefined;
|
||||
@@ -55,7 +55,7 @@ export class XmlWriter {
|
||||
tag: string,
|
||||
props?: Record<string, string>,
|
||||
content?: string,
|
||||
children?: NonNullable<unknown>
|
||||
children?: NonNullable<unknown>,
|
||||
) {
|
||||
const top = this.#stack.at(-1);
|
||||
if (top && !top.children) {
|
||||
@@ -104,7 +104,7 @@ export class XmlWriter {
|
||||
add(
|
||||
tag: string,
|
||||
props: Record<string, string>,
|
||||
children: (writer: XmlWriter) => void
|
||||
children: (writer: XmlWriter) => void,
|
||||
): XmlWriter;
|
||||
add(
|
||||
tag: string,
|
||||
@@ -125,23 +125,21 @@ export class XmlWriter {
|
||||
addAsync(
|
||||
tag: string,
|
||||
props: Record<string, string>,
|
||||
content: string
|
||||
content: string,
|
||||
): Promise<void>;
|
||||
addAsync(
|
||||
tag: string,
|
||||
children: (writer: XmlWriter) => Promise<void>
|
||||
children: (writer: XmlWriter) => Promise<void>,
|
||||
): Promise<void>;
|
||||
addAsync(
|
||||
tag: string,
|
||||
props: Record<string, string>,
|
||||
children: (writer: XmlWriter) => Promise<void>
|
||||
children: (writer: XmlWriter) => Promise<void>,
|
||||
): Promise<void>;
|
||||
async addAsync(
|
||||
tag: string,
|
||||
...rest: (
|
||||
| Record<string, string>
|
||||
| string
|
||||
| ((writer: XmlWriter) => Promise<void>)
|
||||
Record<string, string> | string | ((writer: XmlWriter) => Promise<void>)
|
||||
)[]
|
||||
): Promise<void> {
|
||||
const [props, content, children] = parseAdd(rest);
|
||||
@@ -164,7 +162,7 @@ export class XmlWriter {
|
||||
| Parameters<InstanceType<typeof XmlWriter>["add"]>
|
||||
| [
|
||||
NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
|
||||
...Parameters<InstanceType<typeof XmlWriter>["add"]>
|
||||
...Parameters<InstanceType<typeof XmlWriter>["add"]>,
|
||||
]
|
||||
): string {
|
||||
let options: ConstructorParameters<typeof XmlWriter>[0];
|
||||
@@ -172,7 +170,7 @@ export class XmlWriter {
|
||||
options = rest.shift()! as ConstructorParameters<typeof XmlWriter>[0];
|
||||
}
|
||||
return new XmlWriter(options).add(
|
||||
...(rest as Parameters<InstanceType<typeof XmlWriter>["add"]>)
|
||||
...(rest as Parameters<InstanceType<typeof XmlWriter>["add"]>),
|
||||
).content;
|
||||
}
|
||||
|
||||
@@ -188,7 +186,7 @@ export class XmlWriter {
|
||||
| Parameters<InstanceType<typeof XmlWriter>["addAsync"]>
|
||||
| [
|
||||
NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
|
||||
...Parameters<InstanceType<typeof XmlWriter>["addAsync"]>
|
||||
...Parameters<InstanceType<typeof XmlWriter>["addAsync"]>,
|
||||
]
|
||||
): Promise<string> {
|
||||
let options: ConstructorParameters<typeof XmlWriter>[0];
|
||||
@@ -197,7 +195,7 @@ export class XmlWriter {
|
||||
}
|
||||
const writer = new XmlWriter(options);
|
||||
await writer.addAsync(
|
||||
...(rest as Parameters<InstanceType<typeof XmlWriter>["addAsync"]>)
|
||||
...(rest as Parameters<InstanceType<typeof XmlWriter>["addAsync"]>),
|
||||
);
|
||||
return writer.content;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
jsonBody({ validate: updateuser, includeParams: ["uid"] }),
|
||||
async (ctx) => {
|
||||
ctx.body = await db.updateUser(ctx.request.body);
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete("/users/:uid", async (ctx) => {
|
||||
await db.deleteUserById(ctx.params.uid);
|
||||
@@ -82,7 +82,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
async (ctx) => {
|
||||
const [apikey, token] = await db.createApikey(ctx.request.body);
|
||||
ctx.body = { apikey, token };
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/users/:uid/apikeys/:kid", async (ctx) => {
|
||||
const apikey = await db.getApikeyById(ctx.params.kid);
|
||||
@@ -118,7 +118,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
jsonBody({ validate: updateabode, includeParams: ["aid"] }),
|
||||
async (ctx) => {
|
||||
ctx.body = await db.updateAbode(ctx.request.body, { uid: ctx.user!.uid });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete("/abodes/:aid", async (ctx) => {
|
||||
await db.deleteAbodeById(ctx.params.aid);
|
||||
@@ -138,7 +138,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
jsonBody({ validate: createnote, includeParams: ["aid"] }),
|
||||
async (ctx) => {
|
||||
ctx.body = await db.createNote(ctx.request.body, { uid: ctx.user!.uid });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.use("/residents", authenticate(db));
|
||||
@@ -152,7 +152,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
ctx.body = await db.createResident(ctx.request.body, {
|
||||
uid: ctx.user!.uid,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/residents/:uid/:aid", async (ctx) => {
|
||||
ctx.body = await db.getResidentById(ctx.params.uid, ctx.params.aid);
|
||||
@@ -164,7 +164,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
ctx.body = await db.updateResident(ctx.request.body, {
|
||||
uid: ctx.user!.uid,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete("/residents/:uid/:aid", async (ctx) => {
|
||||
await db.deleteResidentById(ctx.params.uid, ctx.params.aid);
|
||||
@@ -192,7 +192,7 @@ export function apirouter(db: BackendDbInterface): KoaRouter {
|
||||
jsonBody({ validate: updatenote, includeParams: ["nid"] }),
|
||||
async (ctx) => {
|
||||
ctx.body = await db.updateNote(ctx.request.body, { uid: ctx.user!.uid });
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete("/notes/:nid", async (ctx) => {
|
||||
await db.deleteNoteById(ctx.params.nid);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function schemarouter(): KoaRouter {
|
||||
url: `${ctx.URL}/${name}.schema.json`,
|
||||
},
|
||||
];
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { ApiInterface } from "../../../src/db/api/ApiInterface.js";
|
||||
import { apiProtocols, isApiUrl, parseApiUrl } from "../../../src/db/api/url.js";
|
||||
import {
|
||||
apiProtocols,
|
||||
isApiUrl,
|
||||
parseApiUrl,
|
||||
} from "../../../src/db/api/url.js";
|
||||
import {
|
||||
NotFoundAbodeError,
|
||||
NotAuthorizedAbodeError,
|
||||
@@ -86,7 +90,7 @@ describe("parseApiUrl", () => {
|
||||
|
||||
it("extra query params become headers", () => {
|
||||
const [, { headers }] = parseApiUrl(
|
||||
"http://example.com?X-Custom-Header=value"
|
||||
"http://example.com?X-Custom-Header=value",
|
||||
);
|
||||
assert.equal(headers["X-Custom-Header"], "value");
|
||||
});
|
||||
@@ -94,7 +98,7 @@ describe("parseApiUrl", () => {
|
||||
it("throws for non-api protocol", () => {
|
||||
assert.throws(
|
||||
() => parseApiUrl("sqlite:///db.sqlite"),
|
||||
/Not an \{abode\+,\}http\{s,\}: protocol/
|
||||
/Not an \{abode\+,\}http\{s,\}: protocol/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -106,18 +110,22 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
|
||||
before(async () => {
|
||||
let nextStatus = 500;
|
||||
respondWith = (s) => { nextStatus = s; };
|
||||
respondWith = (s) => {
|
||||
nextStatus = s;
|
||||
};
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(nextStatus, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: "test" }));
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
await new Promise<void>((resolve) =>
|
||||
server.listen(0, "127.0.0.1", resolve),
|
||||
);
|
||||
const { port } = server.address() as AddressInfo;
|
||||
serverUrl = `http://127.0.0.1:${port}`;
|
||||
closeServer = () =>
|
||||
new Promise<void>((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve()))
|
||||
server.close((err) => (err ? reject(err) : resolve())),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -131,7 +139,7 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +151,7 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotAuthorizedAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -155,7 +163,7 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
(err) => {
|
||||
assert.ok(err instanceof ReadonlyAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -167,7 +175,7 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
(err) => {
|
||||
assert.ok(err instanceof InvalidAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,7 +187,7 @@ describe("ApiInterface HTTP error mapping", () => {
|
||||
(err) => {
|
||||
assert.ok(err instanceof ConflictAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +92,11 @@ describe("api backend: auth over HTTP", async () => {
|
||||
const self = await fetch(`${server.url}/auth/self`, {
|
||||
headers: { Cookie: `abode_session=${cookie}` },
|
||||
});
|
||||
assert.equal(self.status, 401, "session was invalidated server-side, not just the cookie cleared");
|
||||
assert.equal(
|
||||
self.status,
|
||||
401,
|
||||
"session was invalidated server-side, not just the cookie cleared",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST /auth/clear-sessions invalidates outstanding session cookies", async () => {
|
||||
|
||||
@@ -10,10 +10,10 @@ import { runAuthTests } from "../../shared/auth.js";
|
||||
|
||||
async function createExpiredApikey(
|
||||
db: BackendDbInterface,
|
||||
uid: string
|
||||
uid: string,
|
||||
): Promise<`at_${string}`> {
|
||||
const si = db as SqliteInterface;
|
||||
const token = (`at_${"e".repeat(32)}`) as `at_${string}`;
|
||||
const token = `at_${"e".repeat(32)}` as `at_${string}`;
|
||||
const kid = crypto.randomUUID();
|
||||
const { sql } = si._;
|
||||
si._.db.run(sql`
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("SqliteMigrator", () => {
|
||||
assert.equal(applied.length, 3);
|
||||
assert.deepEqual(
|
||||
applied.map((m) => m.id),
|
||||
[1, 2, 3]
|
||||
[1, 2, 3],
|
||||
);
|
||||
db.destroy();
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe("SqliteMigrator", () => {
|
||||
const migrator = new SqliteMigrator(db);
|
||||
await assert.rejects(
|
||||
() => migrator.migrateTo(9999),
|
||||
/No known migration with id 9999/
|
||||
/No known migration with id 9999/,
|
||||
);
|
||||
db.destroy();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { sql, catSql, joinSql, calcUpdates, unsafeSql } from "../../../src/db/sqlite/sql.js";
|
||||
import {
|
||||
sql,
|
||||
catSql,
|
||||
joinSql,
|
||||
calcUpdates,
|
||||
unsafeSql,
|
||||
} from "../../../src/db/sqlite/sql.js";
|
||||
|
||||
describe("sql template tag", () => {
|
||||
it("produces correct sql and empty vars for plain text", () => {
|
||||
|
||||
@@ -10,13 +10,19 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
|
||||
before(() => {
|
||||
db = makeDb();
|
||||
db.run(unsafeSql("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, val TEXT NOT NULL)"));
|
||||
db.run(
|
||||
unsafeSql(
|
||||
"CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
after(() => db.destroy());
|
||||
|
||||
it("run INSERT returns changes count", () => {
|
||||
const { changes } = db.run(sql`INSERT INTO test(val) VALUES(${{ text: "hello" }})`);
|
||||
const { changes } = db.run(
|
||||
sql`INSERT INTO test(val) VALUES(${{ text: "hello" }})`,
|
||||
);
|
||||
assert.equal(changes, 1);
|
||||
});
|
||||
|
||||
@@ -24,7 +30,9 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
db.run(unsafeSql("DELETE FROM test"));
|
||||
db.run(sql`INSERT INTO test(val) VALUES(${{ text: "a" }})`);
|
||||
db.run(sql`INSERT INTO test(val) VALUES(${{ text: "b" }})`);
|
||||
const rows = db.all<{ val: string }>(unsafeSql("SELECT val FROM test ORDER BY val"));
|
||||
const rows = db.all<{ val: string }>(
|
||||
unsafeSql("SELECT val FROM test ORDER BY val"),
|
||||
);
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].val, "a");
|
||||
assert.equal(rows[1].val, "b");
|
||||
@@ -38,7 +46,7 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
assert.equal(row.val, "one");
|
||||
|
||||
const none = db.get<{ val: string }>(
|
||||
sql`SELECT val FROM test WHERE val = ${{ text: "none" }}`
|
||||
sql`SELECT val FROM test WHERE val = ${{ text: "none" }}`,
|
||||
);
|
||||
assert.equal(none, null);
|
||||
});
|
||||
@@ -49,7 +57,7 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
db.run(sql`INSERT INTO test(val) VALUES(${{ text: "dup2" }})`);
|
||||
assert.throws(
|
||||
() => db.get<{ val: string }>(unsafeSql("SELECT val FROM test")),
|
||||
/Multiple results/
|
||||
/Multiple results/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,7 +77,7 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
db.multi(() => {
|
||||
db.run(sql`INSERT INTO test(val) VALUES(${{ text: "rollback" }})`);
|
||||
throw new Error("abort!");
|
||||
})
|
||||
}),
|
||||
);
|
||||
const rows = db.all<{ val: string }>(unsafeSql("SELECT val FROM test"));
|
||||
assert.equal(rows.length, 0);
|
||||
@@ -82,7 +90,13 @@ function runWrappedDbSuite(name: string, makeDb: () => WrappedDb) {
|
||||
|
||||
it("rethrow propagates non-SQLite errors unchanged", () => {
|
||||
const err = new Error("custom error");
|
||||
assert.throws(() => db.rethrow(() => { throw err; }), (e) => e === err);
|
||||
assert.throws(
|
||||
() =>
|
||||
db.rethrow(() => {
|
||||
throw err;
|
||||
}),
|
||||
(e) => e === err,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -93,9 +107,7 @@ describe("better-sqlite3 WrappedDb", async () => {
|
||||
let bs3Ctor: (new (path: string) => WrappedDb) | null = null;
|
||||
|
||||
try {
|
||||
const mod = await import(
|
||||
"../../../src/db/sqlite/impl/better-sqlite3.js"
|
||||
);
|
||||
const mod = await import("../../../src/db/sqlite/impl/better-sqlite3.js");
|
||||
bs3Ctor = mod.WrappedBetterSqlite3Db;
|
||||
} catch {
|
||||
// better-sqlite3 not available, skip
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ export interface TestServer {
|
||||
}
|
||||
|
||||
export async function createTestServer(
|
||||
db: BackendDbInterface
|
||||
db: BackendDbInterface,
|
||||
): Promise<TestServer> {
|
||||
const app = new Koa();
|
||||
const router = apirouter(db);
|
||||
@@ -23,7 +23,7 @@ export async function createTestServer(
|
||||
url: `http://127.0.0.1:${port}`,
|
||||
close: () =>
|
||||
new Promise<void>((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve()))
|
||||
server.close((err) => (err ? reject(err) : resolve())),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
+27
-12
@@ -1,12 +1,15 @@
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { DbInterface } from "../../src/db/types/DbInterface.js";
|
||||
import { NotFoundAbodeError, InvalidAbodeError } from "../../src/db/types/errors.js";
|
||||
import {
|
||||
NotFoundAbodeError,
|
||||
InvalidAbodeError,
|
||||
} from "../../src/db/types/errors.js";
|
||||
import { hashPassword } from "../../src/util/hash.js";
|
||||
|
||||
export function runAbodeTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>,
|
||||
): void {
|
||||
describe(`${name}: abodes`, async () => {
|
||||
let db: DbInterface;
|
||||
@@ -28,7 +31,10 @@ export function runAbodeTests(
|
||||
after(() => close());
|
||||
|
||||
it("createAbode returns an Abode with expected fields", async () => {
|
||||
const abode = await db.createAbode({ name: "Test Abode" }, { uid: ctxUid });
|
||||
const abode = await db.createAbode(
|
||||
{ name: "Test Abode" },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
assert.ok(abode.aid, "has aid");
|
||||
assert.equal(abode.name, "Test Abode");
|
||||
assert.ok(abode.created_at);
|
||||
@@ -36,7 +42,10 @@ export function runAbodeTests(
|
||||
});
|
||||
|
||||
it("getAbodeById returns the created abode", async () => {
|
||||
const created = await db.createAbode({ name: "ById Abode" }, { uid: ctxUid });
|
||||
const created = await db.createAbode(
|
||||
{ name: "ById Abode" },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
const found = await db.getAbodeById(created.aid);
|
||||
assert.equal(found.aid, created.aid);
|
||||
assert.equal(found.name, "ById Abode");
|
||||
@@ -48,14 +57,14 @@ export function runAbodeTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("listAbodes includes the created abode", async () => {
|
||||
const created = await db.createAbode(
|
||||
{ name: `Listed Abode ${Date.now()}` },
|
||||
{ uid: ctxUid }
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
const abodes = await db.listAbodes();
|
||||
assert.ok(Array.isArray(abodes));
|
||||
@@ -67,32 +76,38 @@ export function runAbodeTests(
|
||||
const created = await db.createAbode({ name: "Before" }, { uid: ctxUid });
|
||||
const updated = await db.updateAbode(
|
||||
{ aid: created.aid, name: "After" },
|
||||
{ uid: ctxUid }
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
assert.equal(updated.aid, created.aid);
|
||||
assert.equal(updated.name, "After");
|
||||
});
|
||||
|
||||
it("updateAbode with no fields throws InvalidAbodeError", async () => {
|
||||
const created = await db.createAbode({ name: "No Update" }, { uid: ctxUid });
|
||||
const created = await db.createAbode(
|
||||
{ name: "No Update" },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
await assert.rejects(
|
||||
() => db.updateAbode({ aid: created.aid }, { uid: ctxUid }),
|
||||
(err) => {
|
||||
assert.ok(err instanceof InvalidAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("deleteAbodeById removes the abode", async () => {
|
||||
const created = await db.createAbode({ name: "To Delete" }, { uid: ctxUid });
|
||||
const created = await db.createAbode(
|
||||
{ name: "To Delete" },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
await db.deleteAbodeById(created.aid);
|
||||
await assert.rejects(
|
||||
() => db.getAbodeById(created.aid),
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,7 +117,7 @@ export function runAbodeTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { hashPassword } from "../../src/util/hash.js";
|
||||
|
||||
export function runApikeyTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>,
|
||||
): void {
|
||||
describe(`${name}: apikeys`, async () => {
|
||||
let db: DbInterface;
|
||||
@@ -69,7 +69,7 @@ export function runApikeyTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ export function runApikeyTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ export function runApikeyTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+15
-7
@@ -11,7 +11,10 @@ import { hashPassword } from "../../src/util/hash.js";
|
||||
export function runAuthTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: BackendDbInterface; close(): void }>,
|
||||
createExpiredApikey?: (db: BackendDbInterface, uid: string) => Promise<`at_${string}`>
|
||||
createExpiredApikey?: (
|
||||
db: BackendDbInterface,
|
||||
uid: string,
|
||||
) => Promise<`at_${string}`>,
|
||||
): void {
|
||||
describe(`${name}: auth`, async () => {
|
||||
let db: BackendDbInterface;
|
||||
@@ -23,7 +26,12 @@ export function runAuthTests(
|
||||
({ db, close } = await getDb());
|
||||
email = `auth-user-${Date.now()}@test.example`;
|
||||
const pw = await hashPassword(password);
|
||||
await db.createUser({ email, name: "Auth User", password: pw, flags: {} });
|
||||
await db.createUser({
|
||||
email,
|
||||
name: "Auth User",
|
||||
password: pw,
|
||||
flags: {},
|
||||
});
|
||||
});
|
||||
|
||||
after(() => close());
|
||||
@@ -40,7 +48,7 @@ export function runAuthTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotAuthorizedAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -54,7 +62,7 @@ export function runAuthTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,7 +79,7 @@ export function runAuthTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof ConflictAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -118,7 +126,7 @@ export function runAuthTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotAuthorizedAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,7 +137,7 @@ export function runAuthTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+21
-12
@@ -1,12 +1,15 @@
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { DbInterface } from "../../src/db/types/DbInterface.js";
|
||||
import { NotFoundAbodeError, InvalidAbodeError } from "../../src/db/types/errors.js";
|
||||
import {
|
||||
NotFoundAbodeError,
|
||||
InvalidAbodeError,
|
||||
} from "../../src/db/types/errors.js";
|
||||
import { hashPassword } from "../../src/util/hash.js";
|
||||
|
||||
export function runResidentTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>,
|
||||
): void {
|
||||
describe(`${name}: residents`, async () => {
|
||||
let db: DbInterface;
|
||||
@@ -36,7 +39,7 @@ export function runResidentTests(
|
||||
uid = resUser.uid;
|
||||
const abode = await db.createAbode(
|
||||
{ name: `Resident Abode ${Date.now()}` },
|
||||
{ uid: ctxUid }
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
aid = abode.aid;
|
||||
await db.createResident({ uid, aid, flags: {} }, { uid: ctxUid });
|
||||
@@ -56,12 +59,12 @@ export function runResidentTests(
|
||||
() =>
|
||||
db.getResidentById(
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
),
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -101,7 +104,7 @@ export function runResidentTests(
|
||||
it("updateResident updates flags", async () => {
|
||||
const updated = await db.updateResident(
|
||||
{ uid, aid, flags: { admin: true } },
|
||||
{ uid: ctxUid }
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
assert.equal(updated.uid, uid);
|
||||
assert.deepEqual(updated.flags, { admin: true });
|
||||
@@ -113,7 +116,7 @@ export function runResidentTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof InvalidAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -125,15 +128,21 @@ export function runResidentTests(
|
||||
password: pw,
|
||||
flags: {},
|
||||
});
|
||||
const abode2 = await db.createAbode({ name: "Del Abode" }, { uid: ctxUid });
|
||||
await db.createResident({ uid: user2.uid, aid: abode2.aid, flags: {} }, { uid: ctxUid });
|
||||
const abode2 = await db.createAbode(
|
||||
{ name: "Del Abode" },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
await db.createResident(
|
||||
{ uid: user2.uid, aid: abode2.aid, flags: {} },
|
||||
{ uid: ctxUid },
|
||||
);
|
||||
await db.deleteResidentById(user2.uid, abode2.aid);
|
||||
await assert.rejects(
|
||||
() => db.getResidentById(user2.uid, abode2.aid),
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -142,12 +151,12 @@ export function runResidentTests(
|
||||
() =>
|
||||
db.deleteResidentById(
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
"00000000-0000-0000-0000-000000000003",
|
||||
),
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { hashPassword } from "../../src/util/hash.js";
|
||||
|
||||
export function runSessionTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: BackendDbInterface; close(): void }>
|
||||
getDb: () => Promise<{ db: BackendDbInterface; close(): void }>,
|
||||
): void {
|
||||
describe(`${name}: sessions`, async () => {
|
||||
let db: BackendDbInterface;
|
||||
@@ -46,7 +46,7 @@ export function runSessionTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ export function runSessionTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+17
-9
@@ -11,7 +11,7 @@ import { hashPassword } from "../../src/util/hash.js";
|
||||
export function runUserTests(
|
||||
name: string,
|
||||
getDb: () => Promise<{ db: DbInterface; close(): void }>,
|
||||
getReadonlyDb?: () => Promise<{ db: DbInterface; close(): void }>
|
||||
getReadonlyDb?: () => Promise<{ db: DbInterface; close(): void }>,
|
||||
): void {
|
||||
describe(`${name}: users`, async () => {
|
||||
let db: DbInterface;
|
||||
@@ -57,13 +57,18 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("getUserByEmail returns the created user", async () => {
|
||||
const email = `user-byemail-${Date.now()}@test.example`;
|
||||
await db.createUser({ email, name: "ByEmail User", password: hashedPw, flags: {} });
|
||||
await db.createUser({
|
||||
email,
|
||||
name: "ByEmail User",
|
||||
password: hashedPw,
|
||||
flags: {},
|
||||
});
|
||||
const found = await db.getUserByEmail(email);
|
||||
assert.ok("email" in found, "result includes email");
|
||||
assert.equal(found.email, email);
|
||||
@@ -75,7 +80,7 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -100,7 +105,10 @@ export function runUserTests(
|
||||
password: hashedPw,
|
||||
flags: {},
|
||||
});
|
||||
const updated = await db.updateUser({ uid: created.uid, name: "After Update" });
|
||||
const updated = await db.updateUser({
|
||||
uid: created.uid,
|
||||
name: "After Update",
|
||||
});
|
||||
assert.equal(updated.uid, created.uid);
|
||||
assert.equal(updated.name, "After Update");
|
||||
});
|
||||
@@ -117,7 +125,7 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof InvalidAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -134,7 +142,7 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -144,7 +152,7 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof NotFoundAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -178,7 +186,7 @@ export function runUserTests(
|
||||
(err) => {
|
||||
assert.ok(err instanceof ReadonlyAbodeError);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+110
-29
@@ -29,7 +29,7 @@ const MOCK_APIKEY: ClientApikey = {
|
||||
};
|
||||
|
||||
function makeMockDb(
|
||||
overrides: Partial<BackendDbInterface> = {}
|
||||
overrides: Partial<BackendDbInterface> = {},
|
||||
): BackendDbInterface {
|
||||
return {
|
||||
readonly: false,
|
||||
@@ -37,42 +37,97 @@ function makeMockDb(
|
||||
name: "mock",
|
||||
close: async () => {},
|
||||
listUsers: async () => [],
|
||||
getUserById: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserById: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
deleteUserById: async () => {},
|
||||
createUser: async () => MOCK_USER,
|
||||
updateUser: async () => MOCK_USER,
|
||||
getUserByEmail: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserByEmail: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
listAbodes: async () => [],
|
||||
getAbodeById: async () => { throw new NotFoundAbodeError(); },
|
||||
getAbodeById: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
deleteAbodeById: async () => {},
|
||||
createAbode: async () => ({ aid: "a", name: "A", created_at: "", created_by: null, updated_at: "", updated_by: null }),
|
||||
updateAbode: async () => ({ aid: "a", name: "A", created_at: "", created_by: null, updated_at: "", updated_by: null }),
|
||||
createAbode: async () => ({
|
||||
aid: "a",
|
||||
name: "A",
|
||||
created_at: "",
|
||||
created_by: null,
|
||||
updated_at: "",
|
||||
updated_by: null,
|
||||
}),
|
||||
updateAbode: async () => ({
|
||||
aid: "a",
|
||||
name: "A",
|
||||
created_at: "",
|
||||
created_by: null,
|
||||
updated_at: "",
|
||||
updated_by: null,
|
||||
}),
|
||||
listResidents: async () => [],
|
||||
getResidentById: async () => { throw new NotFoundAbodeError(); },
|
||||
getResidentById: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
deleteResidentById: async () => {},
|
||||
createResident: async () => ({ uid: "", aid: "", flags: {}, created_at: "", created_by: null, updated_at: "", updated_by: null }),
|
||||
updateResident: async () => ({ uid: "", aid: "", flags: {}, created_at: "", created_by: null, updated_at: "", updated_by: null }),
|
||||
createResident: async () => ({
|
||||
uid: "",
|
||||
aid: "",
|
||||
flags: {},
|
||||
created_at: "",
|
||||
created_by: null,
|
||||
updated_at: "",
|
||||
updated_by: null,
|
||||
}),
|
||||
updateResident: async () => ({
|
||||
uid: "",
|
||||
aid: "",
|
||||
flags: {},
|
||||
created_at: "",
|
||||
created_by: null,
|
||||
updated_at: "",
|
||||
updated_by: null,
|
||||
}),
|
||||
listResidentsByUserId: async () => [],
|
||||
listResidentsByAbodeId: async () => [],
|
||||
listUsersByAbodeId: async () => [],
|
||||
listAbodesByUserId: async () => [],
|
||||
listNotes: async () => [],
|
||||
getNoteById: async () => { throw new NotFoundAbodeError(); },
|
||||
getNoteById: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
deleteNoteById: async () => {},
|
||||
createNote: async () => { throw new Error("unimplemented"); },
|
||||
updateNote: async () => { throw new Error("unimplemented"); },
|
||||
createNote: async () => {
|
||||
throw new Error("unimplemented");
|
||||
},
|
||||
updateNote: async () => {
|
||||
throw new Error("unimplemented");
|
||||
},
|
||||
listNotesByAbodeId: async () => [],
|
||||
listNotesByUserId: async () => [],
|
||||
deleteSessionsByUser: async () => {},
|
||||
listApikeysByUser: async () => [],
|
||||
getApikeyById: async () => { throw new NotFoundAbodeError(); },
|
||||
createApikey: async () => [MOCK_APIKEY, "at_" + "0".repeat(32) as `at_${string}`],
|
||||
getApikeyById: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
createApikey: async () => [
|
||||
MOCK_APIKEY,
|
||||
("at_" + "0".repeat(32)) as `at_${string}`,
|
||||
],
|
||||
deleteApikeyById: async () => {},
|
||||
getUserByLogin: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserBySession: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserByLogin: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
getUserBySession: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
createSession: async () => `as_${"0".repeat(32)}`,
|
||||
deleteSession: async () => {},
|
||||
getUserByApikey: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserByApikey: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -92,7 +147,10 @@ type MockCtx = {
|
||||
};
|
||||
};
|
||||
|
||||
function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrides: Record<string, string> = {}): MockCtx {
|
||||
function makeMockCtx(
|
||||
headerOverrides: Record<string, string> = {},
|
||||
cookieOverrides: Record<string, string> = {},
|
||||
): MockCtx {
|
||||
const clearedCookies = new Set<string>();
|
||||
const ctx: MockCtx = {
|
||||
headers: headerOverrides,
|
||||
@@ -110,7 +168,10 @@ function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrid
|
||||
return cookieOverrides[name];
|
||||
},
|
||||
set(name: string, value: string, opts?: unknown) {
|
||||
if (value === "" || (opts && (opts as { expires?: Date }).expires?.getFullYear()! < 2000)) {
|
||||
if (
|
||||
value === "" ||
|
||||
(opts && (opts as { expires?: Date }).expires?.getFullYear()! < 2000)
|
||||
) {
|
||||
clearedCookies.add(name);
|
||||
}
|
||||
},
|
||||
@@ -121,11 +182,13 @@ function makeMockCtx(headerOverrides: Record<string, string> = {}, cookieOverrid
|
||||
|
||||
async function runMiddleware(
|
||||
db: BackendDbInterface,
|
||||
ctx: MockCtx
|
||||
ctx: MockCtx,
|
||||
): Promise<boolean> {
|
||||
let nextCalled = false;
|
||||
const mw = authenticate(db);
|
||||
await mw(ctx as any, async () => { nextCalled = true; });
|
||||
await mw(ctx as any, async () => {
|
||||
nextCalled = true;
|
||||
});
|
||||
return nextCalled;
|
||||
}
|
||||
|
||||
@@ -157,7 +220,9 @@ describe("authenticate middleware", () => {
|
||||
|
||||
it("wrong password (NotAuthorizedAbodeError) → 401 invalid_password", async () => {
|
||||
const db = makeMockDb({
|
||||
getUserByLogin: async () => { throw new NotAuthorizedAbodeError(); },
|
||||
getUserByLogin: async () => {
|
||||
throw new NotAuthorizedAbodeError();
|
||||
},
|
||||
});
|
||||
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("user:wrong") });
|
||||
await runMiddleware(db, ctx);
|
||||
@@ -167,9 +232,13 @@ describe("authenticate middleware", () => {
|
||||
|
||||
it("unknown user (NotFoundAbodeError) → 401 unknown_user", async () => {
|
||||
const db = makeMockDb({
|
||||
getUserByLogin: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserByLogin: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
});
|
||||
const ctx = makeMockCtx({
|
||||
Authorization: "Basic " + btoa("nobody:pass"),
|
||||
});
|
||||
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("nobody:pass") });
|
||||
await runMiddleware(db, ctx);
|
||||
assert.equal(ctx.status, 401);
|
||||
assert.deepEqual((ctx.body as any)?.error, "unknown_user");
|
||||
@@ -177,7 +246,9 @@ describe("authenticate middleware", () => {
|
||||
|
||||
it("ConflictAbodeError (#unset password) → 401 user_not_loggable", async () => {
|
||||
const db = makeMockDb({
|
||||
getUserByLogin: async () => { throw new ConflictAbodeError(); },
|
||||
getUserByLogin: async () => {
|
||||
throw new ConflictAbodeError();
|
||||
},
|
||||
});
|
||||
const ctx = makeMockCtx({ Authorization: "Basic " + btoa("user:pass") });
|
||||
await runMiddleware(db, ctx);
|
||||
@@ -211,7 +282,9 @@ describe("authenticate middleware", () => {
|
||||
|
||||
it("invalid/expired at_ token → 401 invalid_apikey", async () => {
|
||||
const db = makeMockDb({
|
||||
getUserByApikey: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserByApikey: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
});
|
||||
const ctx = makeMockCtx({ Authorization: `Bearer ${validToken}` });
|
||||
await runMiddleware(db, ctx);
|
||||
@@ -246,17 +319,25 @@ describe("authenticate middleware", () => {
|
||||
const db = makeMockDb();
|
||||
const ctx = makeMockCtx({}, { abode_session: "not-a-session-token" });
|
||||
await runMiddleware(db, ctx);
|
||||
assert.ok(ctx.clearedCookies.has("abode_session"), "cookie should be cleared");
|
||||
assert.ok(
|
||||
ctx.clearedCookies.has("abode_session"),
|
||||
"cookie should be cleared",
|
||||
);
|
||||
assert.equal(ctx.status, 401);
|
||||
});
|
||||
|
||||
it("expired/unknown session token clears cookie and returns 401", async () => {
|
||||
const db = makeMockDb({
|
||||
getUserBySession: async () => { throw new NotFoundAbodeError(); },
|
||||
getUserBySession: async () => {
|
||||
throw new NotFoundAbodeError();
|
||||
},
|
||||
});
|
||||
const ctx = makeMockCtx({}, { abode_session: validToken });
|
||||
await runMiddleware(db, ctx);
|
||||
assert.ok(ctx.clearedCookies.has("abode_session"), "cookie should be cleared");
|
||||
assert.ok(
|
||||
ctx.clearedCookies.has("abode_session"),
|
||||
"cookie should be cleared",
|
||||
);
|
||||
assert.equal(ctx.status, 401);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,9 @@ describe("convertError middleware", () => {
|
||||
it("does not interfere when next succeeds", async () => {
|
||||
const ctx = makeCtx();
|
||||
let nextCalled = false;
|
||||
await convertError(ctx as any, async () => { nextCalled = true; });
|
||||
await convertError(ctx as any, async () => {
|
||||
nextCalled = true;
|
||||
});
|
||||
assert.equal(nextCalled, true);
|
||||
assert.equal(ctx.status, 200);
|
||||
});
|
||||
|
||||
+18
-12
@@ -8,7 +8,7 @@ import { jsonBody } from "../../src/webapi/middleware/jsonBody.js";
|
||||
|
||||
async function request(
|
||||
url: string,
|
||||
opts: { method?: string; body?: unknown; contentType?: string } = {}
|
||||
opts: { method?: string; body?: unknown; contentType?: string } = {},
|
||||
): Promise<{ status: number; body: unknown }> {
|
||||
const method = opts.method ?? "POST";
|
||||
const bodyStr =
|
||||
@@ -21,18 +21,19 @@ async function request(
|
||||
const res = await fetch(url, { method, headers, body: bodyStr });
|
||||
const text = await res.text();
|
||||
let parsed: unknown;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
return { status: res.status, body: parsed };
|
||||
}
|
||||
|
||||
async function makeTestServer() {
|
||||
const failValidator = Object.assign(
|
||||
(_obj: unknown): _obj is never => false,
|
||||
{
|
||||
const failValidator = Object.assign((_obj: unknown): _obj is never => false, {
|
||||
errors: [{ message: "required" }] as unknown[],
|
||||
schema: { $id: "test-schema", title: "Test", description: "" },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const app = new Koa();
|
||||
const router = new KoaRouter();
|
||||
@@ -48,7 +49,7 @@ async function makeTestServer() {
|
||||
async (ctx) => {
|
||||
ctx.status = 200;
|
||||
ctx.body = { ok: true };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
@@ -57,7 +58,7 @@ async function makeTestServer() {
|
||||
async (ctx) => {
|
||||
ctx.status = 200;
|
||||
ctx.body = { ok: true, body: ctx.request.body };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.use(router.routes());
|
||||
@@ -69,7 +70,7 @@ async function makeTestServer() {
|
||||
const url = `http://127.0.0.1:${port}`;
|
||||
const close = () =>
|
||||
new Promise<void>((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve()))
|
||||
server.close((err) => (err ? reject(err) : resolve())),
|
||||
);
|
||||
return { url, close };
|
||||
}
|
||||
@@ -107,11 +108,16 @@ describe("jsonBody middleware", () => {
|
||||
});
|
||||
|
||||
it("failing validator → 400 jsonchema_validation_failed with schema and errors", async () => {
|
||||
const res = await request(url + "/fail-validate", { body: { any: "thing" } });
|
||||
const res = await request(url + "/fail-validate", {
|
||||
body: { any: "thing" },
|
||||
});
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal((res.body as any).error, "jsonchema_validation_failed");
|
||||
assert.ok((res.body as any).schema, "response includes schema");
|
||||
assert.ok(Array.isArray((res.body as any).errors), "response includes errors");
|
||||
assert.ok(
|
||||
Array.isArray((res.body as any).errors),
|
||||
"response includes errors",
|
||||
);
|
||||
});
|
||||
|
||||
it("includeParams: param absent from body → merged in", async () => {
|
||||
|
||||
+10
-10
@@ -56,7 +56,7 @@ export default async (): Promise<Configuration[]> => {
|
||||
(await readdir(file("./src/bin"))).map((bin) => [
|
||||
bin.replace(/\..+$/, ""),
|
||||
file(`./src/bin/${bin}`),
|
||||
])
|
||||
]),
|
||||
);
|
||||
for (const bin of Object.keys(binaries)) {
|
||||
copies.push({
|
||||
@@ -93,12 +93,12 @@ export default async (): Promise<Configuration[]> => {
|
||||
} else if (dbSources === "static") {
|
||||
console.log("Resolving db interfaces statically");
|
||||
aliases[file("./src/db/dbSources.ts")] = file(
|
||||
"./src/db/dbSources.static.ts"
|
||||
"./src/db/dbSources.static.ts",
|
||||
);
|
||||
} else {
|
||||
console.log("Resolving db interfaces shared");
|
||||
aliases[file("./src/db/dbSources.ts")] = file(
|
||||
"./src/db/dbSources.shared.ts"
|
||||
"./src/db/dbSources.shared.ts",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,21 +111,21 @@ export default async (): Promise<Configuration[]> => {
|
||||
} else if (disableBs3) {
|
||||
console.log("Disabling better-sqlite3 sqlite db backend");
|
||||
aliases[file("./src/db/sqlite/impl/implementations.ts")] = file(
|
||||
"./src/db/sqlite/impl/implementations.node.ts"
|
||||
"./src/db/sqlite/impl/implementations.node.ts",
|
||||
);
|
||||
compiledSources.push("sqlite");
|
||||
} else if (disableNodeSqlite) {
|
||||
console.log("Disabling node:sqlite sqlite db backend");
|
||||
aliases[file("./src/db/sqlite/impl/implementations.ts")] = file(
|
||||
"./src/db/sqlite/impl/implementations.bs3.ts"
|
||||
"./src/db/sqlite/impl/implementations.bs3.ts",
|
||||
);
|
||||
compiledSources.push("sqlite");
|
||||
} else {
|
||||
console.log(
|
||||
"Enabling sqlite db interface with better-sqlite3 and node:sqlite backends"
|
||||
"Enabling sqlite db interface with better-sqlite3 and node:sqlite backends",
|
||||
);
|
||||
aliases[file("./src/db/sqlite/impl/implementations.ts")] = file(
|
||||
"./src/db/sqlite/impl/implementations.all.ts"
|
||||
"./src/db/sqlite/impl/implementations.all.ts",
|
||||
);
|
||||
compiledSources.push("sqlite");
|
||||
}
|
||||
@@ -145,18 +145,18 @@ export default async (): Promise<Configuration[]> => {
|
||||
defines.compiledSources = JSON.stringify(compiledSources);
|
||||
for (const source of existingSources)
|
||||
defines[`compiledSources.${source}`] = JSON.stringify(
|
||||
compiledSources.includes(source)
|
||||
compiledSources.includes(source),
|
||||
);
|
||||
if (!compiledSources.length) {
|
||||
console.warn(
|
||||
"No db interface enabled, the builds will be completely useless"
|
||||
"No db interface enabled, the builds will be completely useless",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// log about the natives we have
|
||||
console.log(
|
||||
`Using ${Object.values(natives).filter(Boolean).length} natives:`
|
||||
`Using ${Object.values(natives).filter(Boolean).length} natives:`,
|
||||
);
|
||||
for (const [key, path] of Object.entries(natives)) {
|
||||
if (path) console.log(`- ${key}: ${path}`);
|
||||
|
||||
Reference in New Issue
Block a user