Browse Source

integrate api into frontend.

mian
Gisle Aune 2 years ago
parent
commit
8e16ab39b5
  1. 4
      api/items.go
  2. 4
      api/projects.go
  3. 6
      api/scope.go
  4. 4
      api/stats.go
  5. 1
      frontend/src/app.d.ts
  6. 7
      frontend/src/hooks.ts
  7. 2
      frontend/src/lib/components/frontpage/ScopeLink.svelte
  8. 17
      frontend/src/lib/config.ts
  9. 37
      frontend/src/lib/database/interfaces.ts
  10. 48
      frontend/src/lib/database/mysql/database.ts
  11. 84
      frontend/src/lib/database/mysql/scopes.ts
  12. 10
      frontend/src/lib/models/item.ts
  13. 7
      frontend/src/lib/models/project.ts
  14. 14
      frontend/src/lib/models/scope.ts
  15. 32
      frontend/src/lib/models/stat.ts
  16. 13
      frontend/src/lib/utils/slugify.ts
  17. 31
      frontend/src/routes/[scope].json.ts
  18. 29
      frontend/src/routes/[scope]/__layout.svelte
  19. 30
      frontend/src/routes/api/[...any]/index.ts
  20. 171
      frontend/src/routes/indexdata.json.ts
  21. 4
      internal/database/mysql/scopes.go
  22. 2
      internal/models/item.go
  23. 8
      internal/models/scope.go

4
api/items.go

@ -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 {

4
api/projects.go

@ -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 {

6
api/scope.go

@ -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

4
api/stats.go

@ -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
frontend/src/app.d.ts

@ -17,6 +17,7 @@ declare global {
// interface Session {}
interface Stuff {
scope?: Scope
scopePath?: string
}
}
}

7
frontend/src/hooks.ts

@ -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 = [];
}

2
frontend/src/lib/components/frontpage/ScopeLink.svelte

@ -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>

17
frontend/src/lib/config.ts

@ -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
frontend/src/lib/database/interfaces.ts

@ -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[]>
}

48
frontend/src/lib/database/mysql/database.ts

@ -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
frontend/src/lib/database/mysql/scopes.ts

@ -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.");
}
}

10
frontend/src/lib/models/item.ts

@ -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[]
}

7
frontend/src/lib/models/project.ts

@ -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 {

14
frontend/src/lib/models/scope.ts

@ -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
frontend/src/lib/models/stat.ts

@ -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
frontend/src/lib/utils/slugify.ts

@ -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
frontend/src/routes/[scope].json.ts

@ -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
}),
}
}

29
frontend/src/routes/[scope]/__layout.svelte

@ -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
frontend/src/routes/api/[...any]/index.ts

@ -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;

171
frontend/src/routes/indexdata.json.ts

@ -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
internal/database/mysql/scopes.go

@ -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)

2
internal/models/item.go

@ -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"`

8
internal/models/scope.go

@ -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 {

Loading…
Cancel
Save