mirror of https://github.com/tycrek/ass
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…
Reference in new issue