Skip to content

Commit

Permalink
Add Dto's for all models (#113)
Browse files Browse the repository at this point in the history
* feat: Add first mapper UserMapper with getDto()

* feat: Rewrote fieldmapper to give more info per field

* feat: Add FeedMapper

* feat: Added SourceMapper

* feat: Add sourcemapper ..

.. set default cli output to json

* feat: Add PlatformMapper and mappings for all platforms

* Dont stringify json in dto

* feat: Add PostMapper

* chore: Minor fixes

* feat: Standardized the output in Fairpost.ts

* feat: Handle CombinedResult prettier

* chore: Remove unused methods

* feat: Properly type things and let Fairpost throw errors if needed

* feat: Add files to sourcemapper

---------

Co-authored-by: pike <[email protected]>
  • Loading branch information
commonpike and pike authored Feb 2, 2025
1 parent bed51fa commit ec3b371
Show file tree
Hide file tree
Showing 25 changed files with 1,440 additions and 350 deletions.
14 changes: 14 additions & 0 deletions docs/NewPlatform.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
If your platform is not yet supported by Fairpost,
you can write your own code to support it.

The hardest part is possibly registering your
instance of the app with the platform, to allow
it to post on your (or your users) behalf. How
that works depends on the platform; ymmv.

## Minimal setup

To add support for a new platform, add a class to `src/platforms`
Expand Down Expand Up @@ -147,6 +152,15 @@ If you want users to be able to finetune the plugin settings,
or even enable additional plugins, read the plugin ids and/or
settings using `User.get(...)`.

### Add input/output for custom settings in your platform

If you want custom settings for your platform to be returned
from, and set by, the various interfaces, add a `settings: FieldMapping`
property to your class describing those settings and in your
constructor, call `this.mapper = new PlatformMapper(this);`.
The mapper will handle the Dto's generated for your platform
using the `settings` you defined.

## A more elaborate setup

As your platform gets bigger, you may want to chunk it
Expand Down
13 changes: 2 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const COMMAND = process.argv[2]?.includes("@")
const DRY_RUN = !!getOption("dry-run");
const OPERATOR = (getOption("operator") as string) ?? "admin";
const TARGETUSER = (getOption("target-user") as string) ?? "";
const OUTPUT = (getOption("output") as string) ?? "text";
const PLATFORMS =
((getOption("platforms") as string)?.split(",") as PlatformId[]) ?? undefined;
const SOURCES = (getOption("sources") as string)?.split(",") ?? undefined;
Expand Down Expand Up @@ -53,7 +52,7 @@ async function main() {
const user = USER ? new User(USER) : undefined;

try {
const { result, report } = await Fairpost.execute(operator, user, COMMAND, {
const output = await Fairpost.execute(operator, user, COMMAND, {
dryrun: DRY_RUN,
targetuser: TARGETUSER,
platforms: PLATFORMS,
Expand All @@ -64,15 +63,7 @@ async function main() {
status: STATUS,
});

switch (OUTPUT) {
case "json": {
console.log(JSON.stringify(result, JSONReplacer, "\t"));
break;
}
default: {
console.log(report);
}
}
console.info(JSON.stringify(output, JSONReplacer, "\t"));
} catch (e) {
console.error((e as Error).message ?? e);
}
Expand Down
120 changes: 120 additions & 0 deletions src/mappers/AbstractMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import User from "../models/User";
import Operator from "../models/Operator";
import { FileInfo } from "../models/Source";
import { PostStatus, PostResult } from "../models/Post";

export interface FieldMapping {
[field: string]: {
type:
| "string"
| "string[]"
| "integer"
| "float"
| "boolean"
| "date"
| "json";
label: string;
get: string[]; // (permissions | any | none)[]
set: string[]; // (permissions | any | none)[]
required?: boolean; // only if settable
default?: string | string[] | number | boolean | Date | object;
};
}
export type Dto<T extends Record<string, unknown> = Record<string, unknown>> = {
[K in keyof T]:
| string
| string[]
| number
| boolean
| FileInfo[]
| PostResult[]
| PostStatus
| undefined;
};

/**
* AbstractMapper - base for all mappers
*
* The mappers are used to create mapped versions of the models;
* for example, to send to or read from a client or a database.
*
* All mappers require a *user* because the user can optionally
* configure what data is visible, fe to the operator.
*
* For get operations, the mapper shall only return the fields
* allowed to be get by the operator.
*
* For set operations, the mapper shall return
* only the fields allowed to be set by the operator.
*
*/

export default abstract class AbstractMapper<ModelDto extends Dto> {
protected user: User;

constructor(user: User) {
this.user = user;
}

protected abstract mapping: FieldMapping;

public getReport(operator: Operator): string {
const dto = this.getDto(operator);
const lines: string[] = [];
for (const field in dto) {
let line = "";
if (field in this.mapping) {
line += this.mapping[field].label + ": ";
} else {
line += field + ": ";
}
if (dto[field] instanceof Array) {
lines.push(line);
(dto[field] as string[]).forEach((item) => {
lines.push(" - " + String(item));
});
} else {
line += String(dto[field]);
lines.push(line);
}
}
return lines.join("\n");
}

/**
* Return a dto based on the operator
* @param operator
* @returns key/value pairs for the dto
*/
abstract getDto(operator: Operator): ModelDto;

/**
* Insert a given dto based on the operator
* @param operator
* @param dto
* @returns boolean success
*/
abstract setDto(operator: Operator, dto: ModelDto): boolean;

protected getDtoFields(
operator: Operator,
operation: "get" | "set",
): string[] {
const permissions = operator.getPermissions(this.user);
const fields = Object.keys(this.mapping).filter((field) => {
if (this.mapping[field][operation].includes("none")) return false;
if (this.mapping[field][operation].includes("any")) return true;
if (
this.mapping[field][operation].some(
(permission) =>
permission in permissions &&
permissions[permission as keyof typeof permissions],
)
) {
return true;
}
return false;
});
return fields;
}
}
140 changes: 140 additions & 0 deletions src/mappers/FeedMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import AbstractMapper from "./AbstractMapper";
import { Dto, FieldMapping } from "./AbstractMapper";
import Operator from "../models/Operator";
import Feed from "../models/Feed";

export interface FeedDto
extends Dto<{
model?: string;
id?: string;
user_id?: string;
path?: string;
platforms?: string[];
sources?: string[];
interval?: number;
}> {}

export default class FeedMapper extends AbstractMapper<FeedDto> {
private feed: Feed;
mapping: FieldMapping = {
model: {
type: "string",
label: "Model",
get: ["any"],
set: ["none"],
},
id: {
type: "string",
label: "ID",
get: ["any"],
set: ["none"],
},
user_id: {
type: "string",
label: "User ID",
get: ["any"],
set: ["none"],
},
path: {
type: "string",
label: "Path",
get: ["manageFeed"],
set: ["none"],
},
platforms: {
type: "string[]",
label: "Enabled platforms",
get: ["manageFeed"],
set: ["manageFeed"],
required: false,
},
sources: {
type: "string[]",
label: "Feed sources",
get: ["manageFeed"],
set: ["none"],
required: false,
},
interval: {
type: "float",
label: "Post interval",
get: ["manageFeed"],
set: ["manageFeed"],
required: false,
},
};

constructor(feed: Feed) {
super(feed.user);
this.feed = feed;
}

/**
* Return a dto based on the operator and operation
* @param operator
* @returns key/value pairs for the dto
*/
getDto(operator: Operator): FeedDto {
const fields = this.getDtoFields(operator, "get");
const dto: FeedDto = {};
fields.forEach((field) => {
switch (field) {
case "model":
dto[field] = "feed";
break;
case "id":
dto[field] = this.feed.id;
break;
case "user_id":
dto[field] = this.user.id;
break;
case "path":
dto[field] = this.feed.path;
break;
case "platforms":
dto[field] = Object.keys(this.feed.platforms);
break;
case "sources":
dto[field] = this.feed.getSources().map((s) => s.id);
break;
case "interval":
dto[field] = this.feed.interval;
break;
}
});
return dto;
}

/**
* Insert a given dto based on the operator
* @param operator
* @param dto
* @returns boolean success
*/
setDto(operator: Operator, dto: FeedDto): boolean {
const fields = this.getDtoFields(operator, "set");
for (const field in dto) {
if (field in fields) {
switch (field) {
case "platforms":
this.user.set(
"settings",
"FEED_PLATFORMS",
(dto[field] as string[]).join(","),
);
break;
case "interval":
this.user.set(
"settings",
"FEED_INTERVAL",
String(dto[field] || 24),
);
break;
}
} else {
throw this.user.error("Unknown field: " + field);
}
}
return true;
}
}
Loading

0 comments on commit ec3b371

Please sign in to comment.