diff --git a/package-lock.json b/package-lock.json
index 5bbcfe1..02cf583 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -336,6 +336,25 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz",
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w=="
},
+ "@types/pg": {
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz",
+ "integrity": "sha512-4+rj7fnidA77jFURNanuPPc1HrQv+RkhI6s+K18G9zOKbOUUpChA/rbNMqFukNuZ89LoIt/I9dAlxf329TjCNw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/pg-types": "*"
+ }
+ },
+ "@types/pg-types": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@types/pg-types/-/pg-types-1.11.4.tgz",
+ "integrity": "sha512-WdIiQmE347LGc1Vq3Ki8sk3iyCuLgnccqVzgxek6gEHp2H0p3MQ3jniIHt+bRODXKju4kNQ+mp53lmP5+/9moQ==",
+ "dev": true,
+ "requires": {
+ "moment": ">=2.14.0"
+ }
+ },
"@types/pino": {
"version": "5.8.10",
"resolved": "https://registry.npmjs.org/@types/pino/-/pino-5.8.10.tgz",
@@ -4778,6 +4797,12 @@
}
}
},
+ "moment": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+ "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
+ "dev": true
+ },
"monet": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/monet/-/monet-0.9.0.tgz",
diff --git a/package.json b/package.json
index 47bfa70..e93cb13 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"http-errors": "^1.7.3",
"luxon": "^1.19.2",
"monet": "^0.9.0",
+ "pg": "^7.12.1",
"pg-promise": "^9.2.1",
"pino": "^5.13.3",
"pug": "^2.0.4",
@@ -48,6 +49,7 @@
"@types/http-errors": "^1.6.2",
"@types/luxon": "^1.15.2",
"@types/node": "^12.7.5",
+ "@types/pg": "^7.11.2",
"@types/pino": "^5.8.10",
"@types/webpack-dev-middleware": "^2.0.3",
"css-loader": "^3.2.0",
diff --git a/sql/migrations/patches/20191007000600-task-schedule-type/down.sql b/sql/migrations/patches/20191007000600-task-schedule-type/down.sql
new file mode 100644
index 0000000..9b11bdf
--- /dev/null
+++ b/sql/migrations/patches/20191007000600-task-schedule-type/down.sql
@@ -0,0 +1 @@
+DROP TYPE task_schedule;
\ No newline at end of file
diff --git a/sql/migrations/patches/20191007000600-task-schedule-type/up.sql b/sql/migrations/patches/20191007000600-task-schedule-type/up.sql
new file mode 100644
index 0000000..0e6155b
--- /dev/null
+++ b/sql/migrations/patches/20191007000600-task-schedule-type/up.sql
@@ -0,0 +1 @@
+CREATE TYPE task_schedule AS ENUM('once', 'daily', 'weekly', 'monthly', 'yearly');
diff --git a/sql/migrations/patches/20191007000601-tasks/down.sql b/sql/migrations/patches/20191007000601-tasks/down.sql
new file mode 100644
index 0000000..87f1ed5
--- /dev/null
+++ b/sql/migrations/patches/20191007000601-tasks/down.sql
@@ -0,0 +1 @@
+DROP TABLE tasks;
diff --git a/sql/migrations/patches/20191007000601-tasks/up.sql b/sql/migrations/patches/20191007000601-tasks/up.sql
new file mode 100644
index 0000000..8106ae3
--- /dev/null
+++ b/sql/migrations/patches/20191007000601-tasks/up.sql
@@ -0,0 +1,14 @@
+CREATE TABLE tasks (
+ id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ owner integer NOT NULL REFERENCES users(id),
+ name text NOT NULL,
+ notes text,
+ schedule task_schedule NOT NULL DEFAULT 'daily',
+ min_frequency integer,
+ max_frequency integer,
+ created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CHECK(min_frequency IS NULL OR max_frequency IS NULL OR max_frequency > min_frequency)
+);
+
+CREATE TRIGGER set_tasks_updated BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE PROCEDURE set_updated_timestamp();
diff --git a/sql/tasks/list.sql b/sql/tasks/list.sql
new file mode 100644
index 0000000..197b70d
--- /dev/null
+++ b/sql/tasks/list.sql
@@ -0,0 +1,14 @@
+SELECT
+ id,
+ name,
+ notes,
+ schedule,
+ min_frequency,
+ max_frequency,
+ created_at
+FROM
+ tasks
+WHERE
+ owner = $1
+ORDER BY created_at ASC
+LIMIT $2 OFFSET $3;
diff --git a/src/api.ts b/src/api.ts
index 7ba9809..b3c4063 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -14,6 +14,7 @@
// along with this program. If not, see .
import { db } from "@kredens/db";
+import { User } from "@kredens/db/models";
import { ApolloServer, AuthenticationError, gql } from "apollo-server-express";
import { Kind } from "graphql/language";
import { GraphQLScalarType, GraphQLScalarTypeConfig } from "graphql/type";
@@ -24,15 +25,40 @@ const typeDefs = gql`
"A simple type for getting started"
hello: String
migrations: [Migration]!
+ user(id: ID): User
}
type Migration {
- id: ID
+ id: ID!
name: String!
applied_at: DateTime!
}
+ type User {
+ id: ID!
+ email: String!
+ tasks: [Task]!
+ }
+
+ type Task {
+ id: ID
+ name: String!
+ notes: String
+ schedule: ScheduleType!
+ min_frequency: Int
+ max_frequency: Int
+ created_at: DateTime!
+ }
+
scalar DateTime
+
+ enum ScheduleType {
+ ONCE
+ DAILY
+ WEEKLY
+ MONTHLY
+ YEARLY
+ }
`;
const dateTimeConfig: GraphQLScalarTypeConfig = {
@@ -56,12 +82,29 @@ const dateTimeConfig: GraphQLScalarTypeConfig = {
const resolvers = {
DateTime: new GraphQLScalarType(dateTimeConfig),
Query: {
- hello: async () => {
- return `Hello, world!`;
- },
- migrations: async () => {
- return db.migrations.applied();
+ hello: () => `Hello, world!`,
+ migrations: () => db.migrations.applied(),
+ user: async (
+ parent: any,
+ { id }: { id: string },
+ context: { user?: User }
+ ) => {
+ if (!context.user || context.user.id !== +id) {
+ throw new AuthenticationError(
+ "You cannot query users other than yourself"
+ );
+ }
+ return db.users.details(+id).then(user => user.orNull());
}
+ },
+ User: {
+ tasks: (user: User) =>
+ db.tasks.list(user.id).then(tasks =>
+ tasks.map(t => ({
+ ...t,
+ schedule: t.schedule.toUpperCase()
+ }))
+ )
}
};
diff --git a/src/db/index.ts b/src/db/index.ts
index 96d0098..a444a5d 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -16,16 +16,23 @@
import {
Extensions,
MigrationRepository,
+ TaskRepository,
UserRepository
} from "@kredens/db/repos";
-
+import { DateTime } from "luxon";
+import pg from "pg";
import pgPromise, { IDatabase, IInitOptions } from "pg-promise";
+const types = pg.types;
+types.setTypeParser(types.builtins.TIMESTAMPTZ, DateTime.fromSQL);
+types.setTypeParser(types.builtins.TIMESTAMP, DateTime.fromSQL);
+
type ExtendedProtocol = IDatabase & Extensions;
const initOptions: IInitOptions = {
extend(obj: ExtendedProtocol, dc: any) {
obj.migrations = new MigrationRepository(obj, pgp);
+ obj.tasks = new TaskRepository(obj, pgp);
obj.users = new UserRepository(obj, pgp);
}
};
diff --git a/src/db/models.ts b/src/db/models.ts
index 4305c16..6405062 100644
--- a/src/db/models.ts
+++ b/src/db/models.ts
@@ -25,3 +25,16 @@ export interface User {
id: number;
email: string;
}
+
+export type ScheduleType = "once" | "daily" | "weekly" | "monthly" | "yearly";
+
+export interface Task {
+ id: number;
+ owner: number;
+ name: string;
+ notes?: string;
+ schedule: ScheduleType;
+ min_frequency?: number;
+ max_frequency?: number;
+ created_at: DateTime;
+}
diff --git a/src/db/repos/index.ts b/src/db/repos/index.ts
index ae7f553..365a072 100644
--- a/src/db/repos/index.ts
+++ b/src/db/repos/index.ts
@@ -14,11 +14,13 @@
// along with this program. If not, see .
import { MigrationRepository } from "@kredens/db/repos/migrations";
+import { TaskRepository } from "@kredens/db/repos/tasks";
import { UserRepository } from "@kredens/db/repos/users";
export interface Extensions {
migrations: MigrationRepository;
users: UserRepository;
+ tasks: TaskRepository;
}
-export { MigrationRepository, UserRepository };
+export { MigrationRepository, UserRepository, TaskRepository };
diff --git a/src/db/repos/migrations.ts b/src/db/repos/migrations.ts
index 001e520..aaa77c8 100644
--- a/src/db/repos/migrations.ts
+++ b/src/db/repos/migrations.ts
@@ -50,7 +50,7 @@ export class MigrationRepository {
public async applied(): Promise {
return this.db.map(sql.applied, [], row => {
return {
- applied_at: DateTime.fromSQL(row.applied_at),
+ applied_at: row.applied_at as DateTime,
id: +row.id,
name: row.name
};
diff --git a/src/db/repos/tasks.ts b/src/db/repos/tasks.ts
new file mode 100644
index 0000000..2c51895
--- /dev/null
+++ b/src/db/repos/tasks.ts
@@ -0,0 +1,32 @@
+// Copyright (C) 2019 ModZero
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { ScheduleType, Task } from "@kredens/db/models";
+import { tasks as sql } from "@kredens/db/sql";
+import { IDatabase, IMain } from "pg-promise";
+
+export class TaskRepository {
+ private db: IDatabase;
+
+ constructor(db: IDatabase, pgp: IMain) {
+ this.db = db;
+ }
+
+ public async list(owner: number): Promise {
+ return this.db
+ .manyOrNone(sql.list, [owner, 10, 0])
+ .then(rows => (rows ? rows : []));
+ }
+}
diff --git a/src/db/repos/users.ts b/src/db/repos/users.ts
index 0daee17..829e9b7 100644
--- a/src/db/repos/users.ts
+++ b/src/db/repos/users.ts
@@ -13,11 +13,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+import { User } from "@kredens/db/models";
import { users as sql } from "@kredens/db/sql";
import argon2 from "argon2";
import { Maybe, None, Some } from "monet";
import { IDatabase, IMain } from "pg-promise";
-import { User } from "@kredens/db/models";
export class UserRepository {
private db: IDatabase;
diff --git a/src/db/sql/index.ts b/src/db/sql/index.ts
index edbb650..a740829 100644
--- a/src/db/sql/index.ts
+++ b/src/db/sql/index.ts
@@ -41,7 +41,11 @@ const users = {
login: sql("users/login.sql")
};
-export { migrations, users };
+const tasks = {
+ list: sql("tasks/list.sql")
+};
+
+export { migrations, users, tasks };
/** Helper for linking to external query files */
function sql(file: string): QueryFile {