Created Writing a database interface (markdown)

master
X 7 months ago
parent 2e954cec66
commit d09d871b95

@ -0,0 +1,363 @@
# Creating a database interface
Ass supports countless\* different database types, but sometimes, you may want to use something else.
Maybe you want to store data in *something else*, something *unique*. In this example we'll just be
implementing a yaml database but you can use anything you'd like.\
<span style="font-size: xx-small">\* may not be countless</span>
## Setting up the interface class
Before you can write an interface, you need to change a few values around ass's source code,
hopefully in the future, it'll use a plugin system for this instead, but this isn't the future,
this is now, lets get to work!
First create the skeleton for interface in `/backend/sql`
```typescript
/// FILE: /backend/sql/yaml.ts
import { AssFile, AssUser, UploadToken } from 'ass';
import { Database, DatabaseTable, DatabaseValue } from './database';
import { log } from '../log';
import YAML from 'yaml';
import fs from 'fs';
import path from 'path';
// Database path
const DB_PATH = path.join('.ass-data/db.yaml');
// The class name should be in the format "<Database kind>Database"
export class YAMLDatabase implements Database {
public open(): Promise<void> {
return Promise.resolve();
}
public close(): Promise<void> {
return Promise.resolve();
}
public configure(): Promise<void> {
return Promise.resolve();
}
public put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void> {
return Promise.resolve();
}
public get(table: DatabaseTable, key: string): Promise<DatabaseValue | undefined> {
return Promise.resolve(undefined);
}
public getAll(table: DatabaseTable): Promise<{ [index: string]: DatabaseValue; }> {
return Promise.resolve({});
}
}
```
Once your skeleton interface is ready, set it up to work with the rest of the code
```typescript
/// FILE: /common/types.d.ts
/// TYPE: ass.DatabaseConfiguration
interface DatabaseConfiguration {
// This should match the filename you chose for the interface
kind: /* (snip) */ | 'yaml';
}
```
```typescript
/// FILE: /backend/app.ts
/// FUNCTION: main
switch (UserConfig.config.database?.kind) {
// (snip)
case 'yaml': // Dont forget to import the skeleton!
await DBManager.use(new YAMLDatabase());
break;
}
```
```typescript
/// FILE: /backend/routers/api.ts
/// FUNCTION: Express POST route for /setup
switch (UserConfig.config.database.kind) {
// (snip)
case 'yaml': // Again, dont forget to import the skeleton
await DBManager.use(new YAMLDatabase());
break;
}
```
```typescript
/// FILE: /backend/data.ts
/// VAR: DBNAMES
const DBNAMES = {
// (snip)
// set this to the display name for your database
'yaml': 'YAML'
};
```
Next, add your database to the setup page
```pug
//- * FILE: /views/setup.pug
block content
.flex.flex-col.items-center
h2.setup-text-section-header.mt-4 Database
.setup-panel
sl-tab-group
//- * YAML (change this)
sl-tab#yaml-tab(slot='nav' panel='yaml') YAML
//- If you have options for your db, put them here
sl-tab-panel(name='yaml')
| you all good!
```
Add your database to the setup page code
```typescript
/// FILE: /frontend/setup.ts
/// FUNCTION: DOMContentLoaded listener
const Elements = {
// (snip)
yamlTab: document.querySelector('#yaml-tab') as SlTab
};
Elements.submitButton.addEventListener('click', async () => {
// (snip)
if (Elements.jsonTab.active) {
...
} else if (Elements.yamlTab.active) {
config.database = {
kind: 'yaml'
};
}
});
```
You now have a bare minimum database interface. Now you can get to
writing the actual database part.
## Defining database structure
Before you write the interface code, you're going to want to figure out
how you want to lay out all the values in the database. Since we're
using yaml, we'll do something like this
```typescript
/// FILE: /backend/sql/yaml.ts
type YAMLDatabaseSchema = {
version: 1;
users: { [index: string]: AssUser };
files: { [index: string]: AssFile };
tokens: { [index: string]: UploadToken };
};
```
## Reading and writing
We're going to be saving and loading the database a lot since it's
just a file, so we should probably write a couple helper functions
to handle that for us.
```typescript
export class YAMLDatabase implements Database {
private _readDB(): YAMLDatabaseSchema {
return YAML.parse(fs.readFileSync(DB_PATH, {
encoding: 'utf8'
}));
}
private _writeDB(db: YAMLDatabaseSchema) {
fs.writeFileSync(DB_PATH, YAML.stringify(db), {
encoding: 'utf8'
});
}
}
```
## Database interaction code
Now that you have the interface skeleton set up, you need to make it
actually do something, a database interface has 6 functions:
### function descriptions blah balh nufadjksf
#### open()
```typescript
function open(): Promise<void>;
```
`open` connects to the database, thats it. It should only resolve once
it's done connecting and it should reject if anything goes wrong.
#### close()
```typescript
function close(): Promise<void>;
```
`close` disconnects from the database. It should only resolve once it's
disconnected and it should reject if anything goes wrong.
#### configure()
```typescript
function configure(): Promise<void>;
```
`configure` prepares the database for use, it doesn't configure the interface, it configures the database itself. Some things it may do include creating tables, updating tables if the schema changed or repairing corrupted data. As usual, it should reject if anything goes wrong.
#### put()
```typescript
function put(table: 'assfiles', key: string, data: AssFile): Promise<void>;
function put(table: 'assusers', key: string, data: AssUser): Promise<void>;
function put(table: 'asstokens', key: string, data: UploadToken): Promise<void>;
function put(table: DatabaseTable, key: string, data: DatabaseValue): Promise<void>;
```
`put` inserts a value into the database, table is either `assfiles`, `assusers` or `asstokens`. depending on the table, the data type is
`AssFile`, `AssUser` or `UploadToken` respectively. It should resolve onve done and reject if something goes wrong. If the key already
exists, it should do nothing and reject.
#### get()
```typescript
function get(table: 'assfiles', key: string): Promise<AssFile | undefined>
function get(table: 'assusers', key: string): Promise<AssUser | undefined>
function get(table: 'asstokens', key: string): Promise<UploadToken | undefined>
function get(table: DatabaseTable, key: string): Promise<DatabaseValue | undefined>
```
`get` gets a value from the database. It should resolve to undefined if the key does not exist and should resolve to the value if it does. If something goes wrong it should reject.
#### getAll()
```typescript
function getAll(table: 'assfiles', key: string): Promise<{ [index: string]: AssFile }>
function getAll(table: 'assusers', key: string): Promise<{ [index: string]: AssUser }>
function getAll(table: 'asstokens', key: string): Promise<{ [index: string]: UploadToken }>
function getAll(table: DatabaseTable, key: string): Promise<{ [index: string]: DatabaseValue }>
```
`getAll` should resolve to an object containing a map of keys to values. It should reject if something goes wrong.
### Database configuration
When a database is selected, `DBManager` calls `db.open()` to connect
to the database, then `db.configure()` to set up/update/repair
database tables. We aren't connecting to anything, so we can just keep
`YAMLDatabase.open` as `Promise.resolve()`. We should set up
`YAMLDatabase.configure` to create the database file if it does not
exist (Bonus: If you want, you can also make it validate the YAML, but
we wont be doing that in this guide)
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: YAMLDatabase.configure
return new Promise(async (resolve, reject) => {
try {
if (!fs.existsSync(DB_PATH)) {
this._writeDB({
version: 1,
users: {},
files: {},
tokens: {}
});
}
resolve();
} catch (err) {
log.error('Failed to verify existence of data files');
reject(err);
}
});
```
### Writing to the database
<!-- fuck this is probably really repetitive -->
`db.put()` has 3 different tables that it can store data into,
we'll need to make it so it handles each of them correctly. We'll
start with the generic blank promise
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: YAMLDatabase.put
return new Promise(async (resolve, reject) => {
try {
resolve();
} catch (err) {
log.error('Failed to insert data');
reject(err);
}
});
```
<!-- idk why i didnt just name them the same thing as the tables -->
We'll need to have a mapping of table names to database properties,
let's add one after `DB_PATH`
```typescript
/// FILE: /backend/sql/yaml.ts
const DB_TABLES: { [index: string]: 'users' | 'files' | 'tokens' } = {
'assusers': 'users',
'assfiles': 'files',
'asstokens': 'tokens'
};
```
Now let's set up `put` to store the data in its dedicated spot
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: promise in YAMLDatabase.put
// Load the database
let db = this._readDB();
// Store the data
db[DB_TABLES[table]][key] = data;
// Save the database
this._writeDB(db);
resolve();
```
We're also going to need to make sure that there isn't any conflict
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: promise in YAMLDatabase.put
let db = this._readDB();
// Check for conflict
if (db[DB_TABLES[table]][key] != undefined) {
return reject(new Error(`Key ${key} in ${table} already exists`));
}
```
### Getting values from the database
`db.get()` is like `db.put()`, but backwards. We can just copy the code and make a few modifications.
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: YAMLDatabase.get
return new Promise(async (resolve, reject) => {
try {
// Load the database
let db = this._readDB();
// Get the thing
resolve(db[DB_TABLES[table]][key]);
} catch (err) {
log.error('Failed to get data');
reject(err);
}
});
```
### Getting all values from the database
`db.all()` just returns an object with all values in the database, you
just do what you did with `db.get()`, but return the table instead of
the value.
```typescript
/// FILE: /backend/sql/yaml.ts
/// FUNCTION: YAMLDatabase.getAll
return new Promise(async (resolve, reject) => {
try {
// Load the database
let db = this._readDB();
// Get the thing
resolve(db[DB_TABLES[table]]);
} catch (err) {
log.error('Failed to get data');
reject(err);
}
});
```
### Finishing up
There you go, you did it. You have a yaml interface. If you configure
ass to use yaml, you should see .ass-data/db.yaml get populated with
images and accounts.
Loading…
Cancel
Save