integrate api into frontend.

This commit is contained in:
2022-05-01 13:04:17 +02:00
parent 39bc4fe7e3
commit 8e16ab39b5
23 changed files with 103 additions and 462 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ import (
func Items(g *gin.RouterGroup, db database.Database) {
g.Use(scopeIDMiddleware(db))
g.GET("/", handler("items", func(c *gin.Context) (res interface{}, err error) {
g.GET("", handler("items", func(c *gin.Context) (res interface{}, err error) {
mode := c.Query("mode")
var fromTime, toTime time.Time
@@ -79,7 +79,7 @@ func Items(g *gin.RouterGroup, db database.Database) {
return db.Items(getScope(c).ID).Find(c.Request.Context(), id)
}))
g.POST("/", handler("item", func(c *gin.Context) (interface{}, error) {
g.POST("", handler("item", func(c *gin.Context) (interface{}, error) {
item := &models.Item{}
err := c.BindJSON(item)
if err != nil {
+2 -2
View File
@@ -12,7 +12,7 @@ import (
func Projects(g *gin.RouterGroup, db database.Database) {
g.Use(scopeIDMiddleware(db))
g.GET("/", handler("projects", func(c *gin.Context) (interface{}, error) {
g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) {
return db.Projects(getScope(c).ID).List(c.Request.Context())
}))
@@ -25,7 +25,7 @@ func Projects(g *gin.RouterGroup, db database.Database) {
return db.Projects(getScope(c).ID).Find(c.Request.Context(), id)
}))
g.POST("/", handler("project", func(c *gin.Context) (interface{}, error) {
g.POST("", handler("project", func(c *gin.Context) (interface{}, error) {
project := &models.Project{}
err := c.BindJSON(project)
if err != nil {
+4 -2
View File
@@ -9,7 +9,7 @@ import (
)
func Scopes(g *gin.RouterGroup, db database.Database) {
g.GET("/", handler("scopes", func(c *gin.Context) (interface{}, error) {
g.GET("", handler("scopes", func(c *gin.Context) (interface{}, error) {
return db.Scopes().ListByUser(c.Request.Context(), auth.UserID(c))
}))
@@ -23,9 +23,11 @@ func Scopes(g *gin.RouterGroup, db database.Database) {
if err != nil {
return nil, err
}
if !scope.HasMember(auth.UserID(c)) {
member := scope.Member(auth.UserID(c))
if member == nil {
return nil, slerrors.NotFound("Scope")
}
scope.DisplayName = member.Name
scope.StatusLabels = models.StatusLabels
return scope, nil
+2 -2
View File
@@ -10,7 +10,7 @@ import (
func Stats(g *gin.RouterGroup, db database.Database) {
g.Use(scopeIDMiddleware(db))
g.GET("/", handler("stats", func(c *gin.Context) (interface{}, error) {
g.GET("", handler("stats", func(c *gin.Context) (interface{}, error) {
return getScope(c).Stats, nil
}))
@@ -28,7 +28,7 @@ func Stats(g *gin.RouterGroup, db database.Database) {
return stat, nil
}))
g.POST("/", handler("stat", func(c *gin.Context) (interface{}, error) {
g.POST("", handler("stat", func(c *gin.Context) (interface{}, error) {
stat := &models.Stat{}
err := c.BindJSON(stat)
if err != nil {
+1
View File
@@ -17,6 +17,7 @@ declare global {
// interface Session {}
interface Stuff {
scope?: Scope
scopePath?: string
}
}
}
+4 -3
View File
@@ -1,11 +1,12 @@
import config from "$lib/config";
import type { ScopeEntry } from "$lib/models/scope";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async({ event, resolve }) => {
if (process.env.STUFFLOG3_USE_DUMMY_USER === "true") {
event.locals.user = {
id: "c11230be-4912-4313-83b0-410a248b5bd1",
name: "Developer",
name: "DevMan",
}
} else {
event.locals.user = {id: "", name: "Guest"};
@@ -14,8 +15,8 @@ export const handle: Handle = async({ event, resolve }) => {
}
if (event.locals.user.id !== "") {
const db = await config.database();
event.locals.scopes = await db.withUser(event.locals.user.id).scopes().list()
const res: {scopes: ScopeEntry[]} = await fetch(`${process.env.STUFFLOG3_API}/api/scopes`).then(r => r.json());
event.locals.scopes = res.scopes;
} else {
event.locals.scopes = [];
}
@@ -6,7 +6,7 @@
export let scope: ScopeEntry
</script>
<a href="/{scope.id}/">
<a href="/{scope.id}-{scope.abbreviation.toLocaleLowerCase()}/">
<CardEntry>
<EntryName subtitle={scope.abbreviation}>{scope.name}</EntryName>
</CardEntry>
+1 -16
View File
@@ -1,18 +1,3 @@
import type { Database } from "./database/interfaces";
import MysqlDB from "./database/mysql/database";
let databasePromise: Promise<Database> | null = null;
let databaseTime: number = 0;
const config = {
database() {
if (databasePromise == null || Date.now() < (databaseTime - 60000)) {
databasePromise = MysqlDB.connectEnv();
databaseTime = Date.now();
}
return databasePromise;
}
};
const config = {};
export default config;
-37
View File
@@ -1,37 +0,0 @@
import type { ScopeEntry, ScopeInput } from "$lib/models/scope";
import type Scope from "$lib/models/scope";
import type { StatEntry } from "$lib/models/stat";
import type Stat from "$lib/models/stat";
export class NotFoundError extends Error {
constructor(subject: string) {
super(`${subject} not found`);
}
}
export interface Database {
userId: string
scopes(): ScopeRepo
stats(scopeId: number): StatRepo
withUser(userId: string): Database
}
export interface ScopeRepo {
userId: string
find(id: number): Promise<Scope>
list(): Promise<ScopeEntry[]>
create(input: ScopeInput): Promise<Scope>
update(id: number, input: Partial<ScopeInput>): Promise<Scope>
delete(id: number): Promise<void>
}
export interface StatRepo {
userId: string
scopeId: number
find(id: number): Promise<Stat>
findEntries(...ids: number[]): Promise<StatEntry[]>
list(): Promise<Stat[]>
}
@@ -1,48 +0,0 @@
import {createPool} from "mysql2/promise"
import type {Pool} from "mysql2/promise";
import type { Database, ScopeRepo, StatRepo } from "../interfaces";
import MysqlDBScopes from "./scopes";
export default class MysqlDB implements Database {
connection: Pool
userId: string;
private constructor(userId: string, connection: Pool) {
this.userId = userId;
this.connection = connection;
}
scopes(): ScopeRepo {
return new MysqlDBScopes(this.connection, this.userId);
}
stats(scopeId: number): StatRepo {
throw new Error("Method not implemented.");
}
withUser(userId: string): Database {
return new MysqlDB(userId, this.connection);
}
static async connectEnv(): Promise<MysqlDB> {
return this.connect(
process.env.STUFFLOG3_MYSQL_HOST,
parseInt(process.env.STUFFLOG3_MYSQL_PORT),
process.env.STUFFLOG3_MYSQL_USERNAME,
process.env.STUFFLOG3_MYSQL_PASSWORD,
process.env.STUFFLOG3_MYSQL_SCHEMA,
)
}
static async connect(host: string, port: number, user: string, password: string, database: string): Promise<MysqlDB> {
const connection = await createPool({
host, user, database, password, port,
waitForConnections: true,
connectionLimit: 20,
queueLimit: 0,
});
return new MysqlDB("", connection);
}
}
-84
View File
@@ -1,84 +0,0 @@
import type {Pool} from "mysql2/promise";
import type Scope from "$lib/models/scope";
import type { ScopeEntry, ScopeInput } from "$lib/models/scope";
import type { ScopeRepo } from "../interfaces";
export default class MysqlDBScopes implements ScopeRepo {
userId: string;
connection: Pool;
constructor(connection: Pool, userId: string) {
this.connection = connection;
this.userId = userId;
}
async find(id: number): Promise<Scope> {
const [[scopeRows], [projectRows], [statRows]] = await Promise.all([
this.connection.execute(`
SELECT scope.*, scope_member.name as display_name
FROM scope
INNER JOIN scope_member ON scope.id = scope_member.scope_id
WHERE scope.id = ?
`, [id]),
this.connection.execute(`
SELECT id,name,status
FROM project
WHERE scope_id = ?
`, [id]).catch(() => []),
this.connection.execute(`
SELECT *
FROM stats
WHERE scope_id = ?
`, [id]).catch(() => []),
]);
const r = scopeRows[0] as any;
if (!r) {
return null;
}
return {
id: r.id,
name: r.name,
abbreviation: r.abbreviation,
displayName: r.display_name,
projects: ((projectRows||[]) as any[]).map((p:any) => ({
id: p.id,
name: p.name,
status: p.status,
})),
stats: ((statRows||[]) as any[]).map((s:any) => ({
id: s.id,
name: s.name,
weight: s.weight,
description: s.description,
allowedAmounts: s.allowed_amounts || void(0),
})),
}
}
async list(): Promise<ScopeEntry[]> {
const [rows] = await this.connection.execute(`
SELECT scope.*, scope_member.name as display_name
FROM scope
INNER JOIN scope_member ON id = scope_id
WHERE user_id = ?
`, [this.userId]);
return (rows as any[]).map(r => ({
id: r.id,
name: r.name,
abbreviation: r.abbreviation,
displayName: r.display_name,
}))
}
create(input: ScopeInput): Promise<Scope> {
throw new Error("Method not implemented.");
}
update(id: number, input: Partial<ScopeInput>): Promise<Scope> {
throw new Error("Method not implemented.");
}
delete(id: number): Promise<void> {
throw new Error("Method not implemented.");
}
}
+7 -3
View File
@@ -4,12 +4,16 @@ import type { StatProgressEntry } from "./stat";
export default interface Item {
id: number
scopeId: number
ownerId: string
projectId?: number
projectRequirementId?: number
name: string
description: string
createdTime: Date
createdBy: string
acquireTime?: Date
createdTime: string
acquireTime?: string
scheduledDate?: string
stats: StatProgressEntry[]
}
+4 -3
View File
@@ -1,17 +1,17 @@
import type Item from "./item";
import type { ScopeEntry } from "./scope";
import type { StatProgressEntry } from "./stat";
import type Status from "./status";
export default interface Project extends ProjectEntry {
description: string
author: string
scope: ScopeEntry
requirements: Requirement[]
}
export interface ProjectEntry {
id: number
ownerId: string
createdTime: string
name: string
status: Status
}
@@ -23,6 +23,7 @@ export interface Requirement {
status: Status
stats: StatProgressEntry[]
items: Item[]
}
export interface StandaloneRequirement extends Requirement {
+5 -9
View File
@@ -1,22 +1,18 @@
import type { ProjectEntry } from "./project";
import type Stat from "./stat";
export default interface Scope extends ScopeEntry {
projects: ProjectEntry[]
stats: Stat[]
}
import type Status from "./status";
export interface ScopeEntry {
id: number
name: string
abbreviation: string
displayName: string
}
export interface ScopeInput {
name: string
abbreviation: string
export default interface Scope extends ScopeEntry {
displayName: string
projects: ProjectEntry[]
stats: Stat[]
statusLabels: Record<Status, string>
}
export interface ScopeMember {
-32
View File
@@ -18,35 +18,3 @@ export interface StatAggregate {
acquired: number
required: number
}
const EXAMPLE2: Stat = {
id: 12,
name: "Assets",
description: "The amount of 'assets' produced in the end. This does not contribute towards the weight",
weight: 0,
}
const EXAMPLE3: Stat = {
id: 12,
name: "Hard Surface Practice",
description: "A value of the asset complexity, indicating how much general modeling practice is involved.",
weight: 0.5,
allowedAmounts: {
"Rote": 1,
"Novel": 2,
"Tutorial": 4,
"Difficult": 7,
}
}
const EXAMPLE: Stat = {
id: 12,
name: "Complexity Points",
description: "A value of the asset complexity, indicating how much general modeling practice is involved.",
weight: 1,
allowedAmounts: {
"Small": 1,
"Moderate": 3,
"Complex": 7,
}
}
+13
View File
@@ -0,0 +1,13 @@
export default function sluggify(s: string): string {
let out = "";
for (const c of s.toLocaleLowerCase()) {
if (c >= 'a' && c <= 'z') {
out += c;
} else if (!":'".includes(c)) {
out += "-";
}
}
return out;
}
-31
View File
@@ -1,31 +0,0 @@
import type { RequestHandler } from "@sveltejs/kit";
import config from "$lib/config";
export const get: RequestHandler = async({params, locals}) => {
const scopeId = parseInt(params.scope);
const db = await config.database();
const scope = await db.withUser(locals.user.id).scopes().find(scopeId)
if (scope === null) {
return {
status: 404,
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: "Scope not found",
}),
}
}
return {
status: 200,
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
scope
}),
}
}
+15 -14
View File
@@ -2,22 +2,21 @@
import type { Load } from "@sveltejs/kit/types/internal";
export const load: Load = async({ fetch, params }) => {
const res = await fetch(`/${params.scope}.json`);
try {
const res: {scope: Scope} = await fetch(`/api/scopes/${params.scope.split("-")[0]}`).then(r => r.json());
if (res.ok) {
const data = await res.json();
return {
props: {
...data,
return {
props: {
...res,
},
stuff: {
scope: data.scope,
scope: res.scope,
scopePath: params.scope,
}
};
}
};
} catch(_) {}
return {fallthrough: true};
return {fallthrough: true} as any;
}
const STATUS_ORDER = [
@@ -38,6 +37,8 @@
import MenuCategory from "$lib/components/layout/MenuCategory.svelte";
import type { ProjectEntry } from "$lib/models/project";
import MenuItem from "$lib/components/layout/MenuItem.svelte";
import {page} from "$app/stores";
import sluggify from "$lib/utils/slugify";
export let scope: Scope;
@@ -56,14 +57,14 @@
<div class="column side">
<Header subtitle={scope.name}>{scope.abbreviation}</Header>
<MenuCategory title="Scope">
<MenuItem href="/{scope.id}">Overview</MenuItem>
<MenuItem href="/{scope.id}/history">History</MenuItem>
<MenuItem href="/{$page. stuff.scopePath}">Overview</MenuItem>
<MenuItem href="/{$page.stuff.scopePath}/history">History</MenuItem>
</MenuCategory>
{#each STATUS_ORDER as status (status)}
{#if projectsByStatus[status].length > 0}
<MenuCategory status={status}>
{#each projectsByStatus[status] as project (project.id)}
<MenuItem href="/{scope.id}/project/{project.id}" subtitle={
<MenuItem href="/{$page.stuff.scopePath}/project/{project.id}-{sluggify(project.name)}" subtitle={
project.name.includes(": ") ? project.name.split(": ")[0] + ": " : ""
}>{
project.name.includes(": ") ? project.name.split(": ").slice(1).join(": ") : project.name
+30
View File
@@ -0,0 +1,30 @@
import type { RequestHandler } from "@sveltejs/kit";
export const get: RequestHandler = async({ request, params }) => {
const proxyUrl = `${process.env.STUFFLOG3_API}/api/${params.any}`;
console.log("PRX", proxyUrl);
const res = await fetch(proxyUrl, {
method: request.method,
headers: {
...request.headers,
},
body: request.body,
});
const result = {
status: res.status,
body: await res.text(),
headers: {},
}
res.headers.forEach((v, k) => {
result.headers[k] = v;
})
return result;
}
export const options = get;
export const post = get;
export const put = get;
export const del = get;
+3 -168
View File
@@ -9,174 +9,9 @@ import type { StandaloneSprint } from "$lib/models/sprint"
export const get: RequestHandler = async({locals}) => {
const scopes: ScopeEntry[] = locals.scopes;
const items: StandaloneItem[] = [
{
id: 1, scope: scopes[0],
name: "Table",
description: "A table for the Redrock rec-room.",
acquireDate: "2022-03-14T00:00:00Z",
createdDate: new Date(Date.now() - 3623511).toISOString(),
stats: [
{id: 1, name: "Asset", weight: 0.2, required: 1, acquired: 1},
{id: 2, name: "Complexity", weight: 1, required: 3, acquired: 5},
],
project: {
id: 443,
name: "3D Maps: Proving the concept",
status: Status.Active
}
},
{
id: 2, scope: scopes[0],
name: "Datapad Set",
description: "I need one, at least, but more is better.",
createdDate: new Date(Date.now() - 3623511).toISOString(),
stats: [
{id: 1, name: "Asset", weight: 0.2, required: 1, acquired: 0},
{id: 2, name: "Complexity", weight: 1, required: 3, acquired: 0},
{id: 3, name: "Hard Surface", weight: 0.333, required: 1, acquired: 0},
],
project: {
id: 443,
name: "3D Maps: Proving the concept",
status: Status.Active
}
},
{
id: 3, scope: scopes[1],
name: "Enila/Renala: Technicalities",
description: "Renala is answering Leah's concern and takes the shift at the Respite.",
createdDate: new Date(Date.now() - 3623511).toISOString(),
stats: [
{id: 1, name: "Story", weight: 3, required: 1, acquired: 0},
{id: 2, name: "Story Word", weight: 0.002, required: 500, acquired: 0},
],
project: {
id: 1,
name: "Background Stories",
status: Status.Active
}
},
]
const requirements: StandaloneRequirement[] = [
{
id: 1,
name: "Basic Furniture",
description: "I need a lot of it.",
project: {
id: 443,
name: "3D Maps: Proving the concept",
status: Status.Active
},
status: Status.Background,
stats: [
{id: 1, name: "Asset", weight: 0.2, acquired: 8, required: 15},
{id: 2, name: "Complexity", weight: 1, acquired: 69, required: 30},
{id: 3, name: "Hard Surface", weight: 0.333, acquired: 20, required: 5},
],
scope: scopes[0],
},
{
id: 2,
name: "Catching Up",
description: "Write a few stories to get back up to speed with less written characters.",
project: {
id: 1,
name: "Background Stories",
status: Status.Active
},
status: Status.Completed,
stats: [
{id: 1, name: "Story", weight: 3, required: 10, acquired: 3},
{id: 2, name: "Story Word", weight: 0.002, required: 5000, acquired: 2173},
{id: 3, name: "Worldbuilding", weight: 0.5, required: 5, acquired: 1},
],
scope: scopes[1],
},
{
id: 3,
name: "Chapter 3",
description: "it's go time",
project: {
id: 1,
name: "Wenera's Job",
status: Status.Active
},
status: Status.Available,
stats: [
{id: 1, name: "Story", weight: 3, required: 5, acquired: 1},
{id: 2, name: "Story Word", weight: 0.002, required: 4000, acquired: 759},
],
scope: scopes[1],
}
]
const sprints: StandaloneSprint[] = [
{
id: 553,
name: "March Sprint",
description: "Hey kid, wanna model some trees?",
from: "2022-03-01T00:00:00+0200",
to: "2022-03-30T23:59:59+0200",
coarse: false,
kind: "stats",
scope: scopes[0],
timed: true,
stats: [
{id: 1, name: "Asset", weight: 0, acquired: 13, required: 15},
{id: 2, name: "Complexity", weight: 1, acquired: 37, required: 30},
{id: 3, name: "Hard Surface", weight: 0.25, acquired: 8, required: 5},
{id: 4, name: "UV Mapping", weight: 0.25, acquired: 3, required: 5},
],
items: [],
requirements: [],
aggregateRequired: 30,
aggregateName: "3D Modeling",
},
{
id: 643,
name: "March Sprint",
description: "Crank out stories",
from: "2022-03-01T00:00:00+0200",
to: "2022-03-30T23:59:59+0200",
coarse: false,
kind: "requirements",
scope: scopes[1],
timed: true,
stats: [],
items: [],
requirements: [
requirements[1],
requirements[2],
],
aggregateRequired: 0,
aggregateName: "",
},
{
id: 771,
name: "March Sprint",
description: "Do these",
from: "2022-03-01T00:00:00+0200",
to: "2022-03-30T23:59:59+0200",
coarse: false,
kind: "items",
scope: scopes[0],
timed: true,
stats: [
{id: 1, name: "Asset", weight: 0.2, required: 2, acquired: 1},
{id: 2, name: "Complexity", weight: 1, required: 8, acquired: 5},
{id: 3, name: "Hard Surface", weight: 0.333, required: 1, acquired: 0},
],
items: [
items[0],
items[1],
],
requirements: [],
aggregateRequired: 0,
aggregateName: "",
}
]
const items: StandaloneItem[] = []
const requirements: StandaloneRequirement[] = []
const sprints: StandaloneSprint[] = []
return {
status: 200,
+4
View File
@@ -32,6 +32,10 @@ func (r *scopeRepository) Find(ctx context.Context, id int, full bool) (*models.
Name: scope.Name,
Abbreviation: scope.Abbreviation,
},
Members: []models.ScopeMember{},
Projects: []models.ProjectEntry{},
Stats: []models.Stat{},
StatusLabels: make(map[models.Status]string),
}
eg, ctx := errgroup.WithContext(ctx)
+1 -1
View File
@@ -5,7 +5,7 @@ import "time"
type Item struct {
ID int `json:"id"`
ScopeID int `json:"scopeId"`
OwnerID string `json:"-"`
OwnerID string `json:"ownerId"`
ProjectID *int `json:"projectId,omitempty"`
ProjectRequirementID *int `json:"projectRequirementId,omitempty"`
+4 -4
View File
@@ -16,10 +16,10 @@ type Scope struct {
ScopeEntry
DisplayName string `json:"displayName"`
Members []ScopeMember `json:"members,omitempty"`
Projects []ProjectEntry `json:"projects,omitempty"`
Stats []Stat `json:"stats,omitempty"`
StatusLabels map[Status]string `json:"statusLabels,omitempty"` // Not stored as part of scope, but may be in the future.
Members []ScopeMember `json:"members"`
Projects []ProjectEntry `json:"projects"`
Stats []Stat `json:"stats"`
StatusLabels map[Status]string `json:"statusLabels"` // Not stored as part of scope, but may be in the future.
}
func (s *Scope) Member(id string) *ScopeMember {