Slightly improve on-boarding flow.
This commit is contained in:
+7
-2
@@ -34,12 +34,17 @@ things or time of others, and be patient where people can't find time.
|
||||
|
||||
### Advice for making good pull requests
|
||||
|
||||
- Don't be afraid to create work-in-progress pull requests to get feedback from others if you are not sure on the best
|
||||
way to solve a problem or get stuck.
|
||||
- Comment on an existing issue or make a new one for what you plan on doing, so others know you are working on it.
|
||||
- Please make sure you run the current version of prettier installed in the project on any files you change to
|
||||
ensure they have the project code style. The easiest way to do this is to set it up with your editor to run
|
||||
automatically, see above.
|
||||
- Don't be afraid to create work-in-progress pull requests to get feedback from others if you are not sure on the best
|
||||
way to solve a problem or get stuck.
|
||||
- If you are installing additional dependencies, please try to keep them minimal (e.g: don't install giant frameworks
|
||||
for one or two small features) and check they don't have vulnerabilities and are trustworthy projects.
|
||||
- Try to keep a clean commit history. Make commits reasonably-sized, self-contained changes that are well described by
|
||||
their commit messages. Git has powerful tools for editing your commit history, so don't feel constrained by this
|
||||
while developing, but remember you can go back with amend commits and interactive rebases to clean it up later.
|
||||
If there are changes while you are working, please rebase on the new head rather than merging. If you are unsure
|
||||
about any of this (git can be intimidating), feel free to just commit up a PR as-is and ask for help.
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ getLanguages : (List String -> Msg) -> Cmd Msg
|
||||
getLanguages handleSuccess =
|
||||
Http.get
|
||||
{ url = apiUrl [ "languages" ]
|
||||
, expect = expectJsonOrError handleSuccess (Json.Decode.list Json.Decode.string)
|
||||
, expect = expectJsonOrError (Tuple.second >> handleSuccess) (Json.Decode.list Json.Decode.string)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,16 +53,20 @@ getAuthMethods : (Auth.Methods -> Msg) -> Cmd Msg
|
||||
getAuthMethods handleSuccess =
|
||||
Http.get
|
||||
{ url = apiUrl [ "auth" ]
|
||||
, expect = expectJsonOrError handleSuccess Auth.decode
|
||||
, expect = expectJsonOrError (Tuple.second >> handleSuccess) Auth.decode
|
||||
}
|
||||
|
||||
|
||||
signIn : Json.Value -> (Auth -> Msg) -> Cmd Msg
|
||||
signIn : Json.Value -> (Bool -> Auth -> Msg) -> Cmd Msg
|
||||
signIn authPayload handleSuccess =
|
||||
let
|
||||
toMsg ( statusCode, auth ) =
|
||||
handleSuccess (statusCode == 201) auth
|
||||
in
|
||||
Http.post
|
||||
{ url = apiUrl [ "users" ]
|
||||
, body = authPayload |> Http.jsonBody
|
||||
, expect = expectJsonOrError handleSuccess Auth.decoder
|
||||
, expect = expectJsonOrError toMsg Auth.decoder
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +74,7 @@ getDeck : Deck.Code -> (Deck -> Msg) -> Cmd Msg
|
||||
getDeck code toMsg =
|
||||
Http.get
|
||||
{ url = apiUrl [ "decks", code |> Deck.codeToString ]
|
||||
, expect = expectJsonOrError toMsg Deck.decode
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) Deck.decode
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +92,7 @@ getDecks id token toMsg =
|
||||
Http.post
|
||||
{ url = apiUrl [ "decks", "by", id ]
|
||||
, body = body |> Json.object |> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg (Decks.codeAndSummaryDecoder |> Json.Decode.list)
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) (Decks.codeAndSummaryDecoder |> Json.Decode.list)
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +107,7 @@ browseDecks { page, language, search } toMsg =
|
||||
in
|
||||
Http.get
|
||||
{ url = queryApiUrl [ "decks", "browse" ] (queries |> List.filterMap identity)
|
||||
, expect = expectJsonOrError toMsg (Decks.codeAndSummaryDecoder |> Json.Decode.list)
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) (Decks.codeAndSummaryDecoder |> Json.Decode.list)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +121,7 @@ createDeck token d toMsg =
|
||||
]
|
||||
|> Json.object
|
||||
|> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg Deck.codeDecoder
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) Deck.codeDecoder
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +132,7 @@ deleteDeck token code toMsg =
|
||||
, headers = []
|
||||
, url = apiUrl [ "decks", code |> Deck.codeToString ]
|
||||
, body = [ ( "token", token |> Json.string ) ] |> Json.object |> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg Deck.codeDecoder
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) Deck.codeDecoder
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
@@ -146,7 +150,7 @@ save token code patch toMsg =
|
||||
]
|
||||
|> Json.object
|
||||
|> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg Deck.decode
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) Deck.decode
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
@@ -174,7 +178,7 @@ saveProfile token name toMsg =
|
||||
]
|
||||
|> Json.object
|
||||
|> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg Auth.decoder
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) Auth.decoder
|
||||
}
|
||||
|
||||
|
||||
@@ -185,17 +189,19 @@ deleteProfile token toMsg =
|
||||
, headers = []
|
||||
, url = apiUrl [ "users" ]
|
||||
, body = [ ( "token", token |> Json.string ) ] |> Json.object |> Http.jsonBody
|
||||
, expect = expectJsonOrError toMsg (Json.Decode.succeed ())
|
||||
, expect = expectJsonOrError (Tuple.second >> toMsg) (Json.Decode.succeed ())
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
expectJsonOrError : (value -> Msg) -> Json.Decode.Decoder value -> Http.Expect Msg
|
||||
expectJsonOrError : (( Int, value ) -> Msg) -> Json.Decode.Decoder value -> Http.Expect Msg
|
||||
expectJsonOrError toMsg decoder =
|
||||
let
|
||||
onGoodStatus =
|
||||
Json.Decode.decodeString decoder >> Result.mapError (Error.BadResponse >> Error.Application)
|
||||
onGoodStatus statusCode =
|
||||
Json.Decode.decodeString decoder
|
||||
>> Result.map (Tuple.pair statusCode)
|
||||
>> Result.mapError (Error.BadResponse >> Error.Application)
|
||||
|
||||
onBadStatus body =
|
||||
case body |> Json.Decode.decodeString Error.decode of
|
||||
@@ -210,11 +216,11 @@ expectJsonOrError toMsg decoder =
|
||||
|
||||
expectBytesOrError : (Bytes -> Msg) -> Http.Expect Msg
|
||||
expectBytesOrError toMsg =
|
||||
handle Ok (Error.ServerError |> Error.Application |> always)
|
||||
handle (always Ok) (Error.ServerError |> Error.Application |> always)
|
||||
|> Http.expectBytesResponse (toMsgHandlingErrors toMsg)
|
||||
|
||||
|
||||
handle : (body -> Result Error value) -> (body -> Error) -> Http.Response body -> Result Error value
|
||||
handle : (Int -> body -> Result Error value) -> (body -> Error) -> Http.Response body -> Result Error value
|
||||
handle onGoodStatus onBadStatus response =
|
||||
case response of
|
||||
Http.BadUrl_ url ->
|
||||
@@ -233,8 +239,8 @@ handle onGoodStatus onBadStatus response =
|
||||
else
|
||||
onBadStatus body |> Err
|
||||
|
||||
Http.GoodStatus_ _ body ->
|
||||
onGoodStatus body
|
||||
Http.GoodStatus_ { statusCode } body ->
|
||||
onGoodStatus statusCode body
|
||||
|
||||
|
||||
toMsgHandlingErrors : (value -> Msg) -> Result Error value -> Msg
|
||||
|
||||
@@ -27,6 +27,12 @@ view viewing { browserLanguage, auth, decks, knownLanguages } =
|
||||
|
||||
renderedDecks =
|
||||
case decks of
|
||||
Just [] ->
|
||||
Html.div [ HtmlA.id "no-decks" ]
|
||||
[ Icon.ghost |> Icon.viewIcon
|
||||
, Html.p [] [ Html.text "You haven't made any decks yet, create or upload one below." ]
|
||||
]
|
||||
|
||||
Just d ->
|
||||
d |> List.map deck |> HtmlK.ul [ HtmlA.class "deck-list" ]
|
||||
|
||||
|
||||
@@ -36,19 +36,27 @@ update msg model =
|
||||
( model, Ports.tryGoogleAuth method.id )
|
||||
|
||||
GoogleAuthResult code ->
|
||||
( model, Api.signIn (Google.authPayload code) (SetAuth >> Global.LoginMsg) )
|
||||
( model, Api.signIn (Google.authPayload code) (\isNewAccount auth -> SetAuth isNewAccount auth |> Global.LoginMsg) )
|
||||
|
||||
TryGuestSignIn _ ->
|
||||
( model, Api.signIn Guest.authPayload (SetAuth >> Global.LoginMsg) )
|
||||
( model, Api.signIn Guest.authPayload (\isNewAccount auth -> SetAuth isNewAccount auth |> Global.LoginMsg) )
|
||||
|
||||
TryTwitchSignIn method ->
|
||||
( model, method |> Twitch.requestUrl model.origin |> Navigation.load )
|
||||
|
||||
SetAuth auth ->
|
||||
SetAuth isNewAccount auth ->
|
||||
let
|
||||
page =
|
||||
if isNewAccount then
|
||||
Profile
|
||||
|
||||
else
|
||||
auth.id |> Decks.List |> Decks
|
||||
in
|
||||
( { model | auth = Just auth, usernameField = auth.name }
|
||||
, Cmd.batch
|
||||
[ auth |> Just |> Auth.store
|
||||
, Route.redirectTo model.navKey (auth.id |> Decks.List |> Decks)
|
||||
, Route.redirectTo model.navKey page
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@ type Msg
|
||||
| GoogleAuthResult String
|
||||
| TryGuestSignIn Guest.Method
|
||||
| TryTwitchSignIn Twitch.Method
|
||||
| SetAuth Auth
|
||||
| SetAuth Bool Auth
|
||||
| SignOut
|
||||
|
||||
@@ -37,7 +37,7 @@ onRouteChanged route oldModel =
|
||||
Twitch.authPayload frag
|
||||
|
||||
signIn payload =
|
||||
Api.signIn payload (Login.SetAuth >> LoginMsg)
|
||||
Api.signIn payload (\isNewAccount auth -> Login.SetAuth isNewAccount auth |> LoginMsg)
|
||||
in
|
||||
( model
|
||||
, parsedPayload
|
||||
|
||||
@@ -84,3 +84,13 @@ ul.deck-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#no-decks {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-25
@@ -13,13 +13,13 @@ import * as Store from "./store";
|
||||
import * as Auth from "./user/auth";
|
||||
import { default as GoogleAuth } from "google-auth-library";
|
||||
import * as Uuid from "uuid";
|
||||
// @ts-ignore
|
||||
import { default as Zip } from "express-easy-zip";
|
||||
import * as Errors from "./errors";
|
||||
import { default as Jwks } from "jwks-rsa";
|
||||
import * as util from "util";
|
||||
import { default as Jwt } from "jsonwebtoken";
|
||||
import { AuthFailure } from "./errors";
|
||||
import { CreatedOrFoundUser } from "./store";
|
||||
|
||||
interface TwitchClaims {
|
||||
sub: string;
|
||||
@@ -55,8 +55,8 @@ const main = async (): Promise<void> => {
|
||||
config.auth.twitch === undefined
|
||||
? undefined
|
||||
: Jwks({
|
||||
jwksUri: config.auth.twitch.jwk,
|
||||
});
|
||||
jwksUri: config.auth.twitch.jwk,
|
||||
});
|
||||
|
||||
const verifyWith = async (
|
||||
token: string,
|
||||
@@ -67,13 +67,15 @@ const main = async (): Promise<void> => {
|
||||
const key = await getPubKey(header.kid as string);
|
||||
return key.getPublicKey();
|
||||
};
|
||||
const decoded = Jwt.decode(token, { complete: true });
|
||||
if (decoded === null || !decoded.hasOwnProperty("header")) {
|
||||
const decoded = Jwt.decode(token, { complete: true }) as {
|
||||
header: Jwt.JwtHeader;
|
||||
};
|
||||
if (decoded !== null && decoded.hasOwnProperty("header")) {
|
||||
const key = await keyFromHeader(decoded.header);
|
||||
return Jwt.verify(token, key, { algorithms: ["RS256"] }) as TwitchClaims;
|
||||
} else {
|
||||
throw new AuthFailure();
|
||||
}
|
||||
// @ts-ignore
|
||||
const key = await keyFromHeader(decoded.header);
|
||||
return Jwt.verify(token, key, { algorithms: ["RS256"] }) as TwitchClaims;
|
||||
};
|
||||
|
||||
const app = express();
|
||||
@@ -103,6 +105,23 @@ const main = async (): Promise<void> => {
|
||||
});
|
||||
|
||||
app.post("/api/users", async (req, res) => {
|
||||
function foundOrCreatedUser({
|
||||
result,
|
||||
user: { id, name },
|
||||
}: Store.CreatedOrFoundUser) {
|
||||
switch (result) {
|
||||
case "Created":
|
||||
res.status(HttpStatus.CREATED);
|
||||
break;
|
||||
case "Found":
|
||||
res.status(HttpStatus.OK);
|
||||
break;
|
||||
default:
|
||||
Store.unknownResult(result);
|
||||
break;
|
||||
}
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
}
|
||||
if (req.body.token !== undefined) {
|
||||
const claims = auth.validate(req.body.token);
|
||||
const id = claims.sub;
|
||||
@@ -120,33 +139,29 @@ const main = async (): Promise<void> => {
|
||||
});
|
||||
const payload = ticket.getPayload();
|
||||
if (payload !== undefined) {
|
||||
const { id, name } = await store.findOrCreateGoogleUser(
|
||||
payload.sub,
|
||||
payload.name
|
||||
foundOrCreatedUser(
|
||||
await store.findOrCreateGoogleUser(payload.sub, payload.name)
|
||||
);
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
}
|
||||
} else if (req.body.twitch) {
|
||||
if (twitch === undefined || config.auth.twitch === undefined) {
|
||||
throw new Errors.AuthFailure();
|
||||
}
|
||||
const claims = await verifyWith(req.body.twitch.id, twitch);
|
||||
const { id, name } = await store.findOrCreateTwitchUser(
|
||||
claims.sub,
|
||||
claims.preferred_username
|
||||
foundOrCreatedUser(
|
||||
await store.findOrCreateTwitchUser(
|
||||
claims.sub,
|
||||
claims.preferred_username
|
||||
)
|
||||
);
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
} else if (req.body.guest) {
|
||||
if (config.auth.guest === undefined) {
|
||||
throw new Errors.AuthFailure();
|
||||
}
|
||||
const { id, name } = await store.findOrCreateGuestUser();
|
||||
res.json({ token: auth.sign({ sub: id }), name });
|
||||
return;
|
||||
foundOrCreatedUser(await store.findOrCreateGuestUser());
|
||||
} else {
|
||||
throw new Errors.AuthFailure();
|
||||
}
|
||||
throw new Errors.AuthFailure();
|
||||
});
|
||||
|
||||
app.delete("/api/users", async (req, res) => {
|
||||
@@ -204,7 +219,6 @@ const main = async (): Promise<void> => {
|
||||
app.post("/api/backup", async (req, res) => {
|
||||
const claims = auth.validate(req.body.token);
|
||||
const files = await store.getDecks(claims.sub);
|
||||
// @ts-ignore
|
||||
res.zip({
|
||||
files: files.map((c) => {
|
||||
const d: any = c;
|
||||
@@ -213,8 +227,9 @@ const main = async (): Promise<void> => {
|
||||
d.author = d.author.name;
|
||||
return {
|
||||
content: Json5.stringify(c, undefined, 2),
|
||||
name: `${d.name.replace(/\s/g, "-")}-${d.language
|
||||
}-${Uuid.v4()}.deck.json5`,
|
||||
name: `${d.name.replace(/\s/g, "-")}-${
|
||||
d.language
|
||||
}-${Uuid.v4()}.deck.json5`,
|
||||
};
|
||||
}),
|
||||
filename: "backup.zip",
|
||||
|
||||
+128
-116
@@ -8,8 +8,21 @@ import * as Errors from "./errors";
|
||||
import * as uuid from "uuid";
|
||||
import * as User from "./user";
|
||||
import * as Code from "./deck/code";
|
||||
import { uniqueNamesGenerator, Config, adjectives, colors, animals } from 'unique-names-generator';
|
||||
import { Console } from "winston/lib/winston/transports";
|
||||
import {
|
||||
uniqueNamesGenerator,
|
||||
adjectives,
|
||||
colors,
|
||||
animals,
|
||||
} from "unique-names-generator";
|
||||
|
||||
export interface CreatedOrFoundUser {
|
||||
result: "Created" | "Found";
|
||||
user: User.User;
|
||||
}
|
||||
|
||||
export function unknownResult(result: never): never {
|
||||
throw new Error(`Unknown result: "${result}".`);
|
||||
}
|
||||
|
||||
const migrateConfig = {
|
||||
logger: (msg: string) => Logging.logger.info(msg),
|
||||
@@ -26,8 +39,8 @@ export const init = async (config: Pg.PoolConfig): Promise<Store> => {
|
||||
const db = config.database
|
||||
? config.database
|
||||
: config.user
|
||||
? config.user
|
||||
: "manydecks";
|
||||
? config.user
|
||||
: "manydecks";
|
||||
await Migrate.createDb(db, { client }, migrateConfig);
|
||||
await Migrate.migrate({ client }, "src/sql", migrateConfig);
|
||||
|
||||
@@ -70,64 +83,66 @@ export class Store {
|
||||
|
||||
generateUsername(): string {
|
||||
return uniqueNamesGenerator({
|
||||
dictionaries: [adjectives, colors, animals]
|
||||
dictionaries: [adjectives, colors, animals],
|
||||
});
|
||||
}
|
||||
|
||||
public findOrCreateGoogleUser: (
|
||||
googleId: string,
|
||||
googleName?: string
|
||||
) => Promise<User.User> = async (googleId, googleName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE google_id = $1;`,
|
||||
[googleId]
|
||||
) => Promise<CreatedOrFoundUser> = async (googleId, googleName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE google_id = $1;`,
|
||||
[googleId]
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { result: "Found", user: { id: user.id, name: user.name } };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName =
|
||||
googleName === undefined ? this.generateUsername() : googleName;
|
||||
await client.query(
|
||||
`INSERT INTO manydecks.users (id, name, google_id) VALUES ($1, $2, $3);`,
|
||||
[newId, newName, googleId]
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { id: user.id, name: user.name };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName = googleName === undefined ? this.generateUsername() : googleName;
|
||||
await client.query(
|
||||
`INSERT INTO manydecks.users (id, name, google_id) VALUES ($1, $2, $3);`,
|
||||
[newId, newName, googleId]
|
||||
);
|
||||
return { id: newId, name: newName };
|
||||
}
|
||||
});
|
||||
return { result: "Created", user: { id: newId, name: newName } };
|
||||
}
|
||||
});
|
||||
|
||||
public findOrCreateTwitchUser: (
|
||||
twitchId: string,
|
||||
twitchName?: string
|
||||
) => Promise<User.User> = async (twitchId, twitchName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE twitch_id = $1;`,
|
||||
[twitchId]
|
||||
) => Promise<CreatedOrFoundUser> = async (twitchId, twitchName) =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE twitch_id = $1;`,
|
||||
[twitchId]
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { result: "Found", user: { id: user.id, name: user.name } };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName =
|
||||
twitchName === undefined ? this.generateUsername() : twitchName;
|
||||
await client.query(
|
||||
`INSERT INTO manydecks.users (id, name, twitch_id) VALUES ($1, $2, $3);`,
|
||||
[newId, newName, twitchId]
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { id: user.id, name: user.name };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName = twitchName === undefined ? this.generateUsername() : twitchName;
|
||||
await client.query(
|
||||
`INSERT INTO manydecks.users (id, name, twitch_id) VALUES ($1, $2, $3);`,
|
||||
[newId, newName, twitchId]
|
||||
);
|
||||
return { id: newId, name: newName };
|
||||
}
|
||||
});
|
||||
return { result: "Created", user: { id: newId, name: newName } };
|
||||
}
|
||||
});
|
||||
|
||||
public findOrCreateGuestUser: () => Promise<User.User> = async () =>
|
||||
public findOrCreateGuestUser: () => Promise<CreatedOrFoundUser> = async () =>
|
||||
await this.withClient(async (client) => {
|
||||
const existingResult = await client.query(
|
||||
`SELECT id, name FROM manydecks.users WHERE is_guest;`
|
||||
);
|
||||
if (existingResult.rowCount > 0) {
|
||||
const [user] = existingResult.rows;
|
||||
return { id: user.id, name: user.name };
|
||||
return { result: "Found", user: { id: user.id, name: user.name } };
|
||||
} else {
|
||||
const newId = User.id();
|
||||
const newName = this.generateUsername();
|
||||
@@ -135,7 +150,7 @@ export class Store {
|
||||
`INSERT INTO manydecks.users (id, name, is_guest) VALUES ($1, $2, True);`,
|
||||
[newId, newName]
|
||||
);
|
||||
return { id: newId, name: newName };
|
||||
return { result: "Created", user: { id: newId, name: newName } };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -159,27 +174,24 @@ export class Store {
|
||||
d: Deck.EditableDeck,
|
||||
user: string
|
||||
) => Promise<number> = async (d, user) =>
|
||||
await this.withClient(async (client) => {
|
||||
let deck;
|
||||
try {
|
||||
deck = Deck.validate(d);
|
||||
} catch (error) {
|
||||
throw new Errors.BadDeck();
|
||||
}
|
||||
const insert = await client.query(
|
||||
`
|
||||
await this.withClient(async (client) => {
|
||||
let deck;
|
||||
try {
|
||||
deck = Deck.validate(d);
|
||||
} catch (error) {
|
||||
throw new Errors.BadDeck();
|
||||
}
|
||||
const insert = await client.query(
|
||||
`
|
||||
INSERT INTO manydecks.decks (deck, author) VALUES ($1, $2) RETURNING id;
|
||||
`,
|
||||
[deck, user]
|
||||
);
|
||||
const [inserted] = insert.rows;
|
||||
return inserted.id;
|
||||
});
|
||||
[deck, user]
|
||||
);
|
||||
const [inserted] = insert.rows;
|
||||
return inserted.id;
|
||||
});
|
||||
|
||||
public getDeck: (id: number, user?: string) => Promise<Deck.Deck> = async (
|
||||
id,
|
||||
user = undefined
|
||||
) =>
|
||||
public getDeck: (id: number) => Promise<Deck.Deck> = async (id) =>
|
||||
await this.withClient(async (client) => {
|
||||
const meta = await client.query(
|
||||
`
|
||||
@@ -254,7 +266,7 @@ export class Store {
|
||||
};
|
||||
});
|
||||
|
||||
private static * summariesFromRows(
|
||||
private static *summariesFromRows(
|
||||
result: Pg.QueryResult
|
||||
): Iterable<Deck.CodeAndSummary> {
|
||||
for (const deck of result.rows) {
|
||||
@@ -284,42 +296,42 @@ export class Store {
|
||||
user,
|
||||
publicOnly = true
|
||||
) =>
|
||||
await this.withClient(async (client) => {
|
||||
const result = await client.query(
|
||||
`
|
||||
await this.withClient(async (client) => {
|
||||
const result = await client.query(
|
||||
`
|
||||
SELECT id, name, author_id, author, language, calls, responses, public, version
|
||||
FROM manydecks.summaries WHERE summaries.author_id = $1 AND (NOT $2 OR summaries.public)
|
||||
ORDER BY id DESC;
|
||||
`,
|
||||
[user, publicOnly]
|
||||
);
|
||||
return Store.summariesFromRows(result);
|
||||
});
|
||||
[user, publicOnly]
|
||||
);
|
||||
return Store.summariesFromRows(result);
|
||||
});
|
||||
|
||||
public browse: (
|
||||
query?: string,
|
||||
language?: string,
|
||||
page?: number
|
||||
) => Promise<Iterable<Deck.CodeAndSummary>> = async (query, language, page) =>
|
||||
await this.withClient(async (client) => {
|
||||
const pageSize = 20;
|
||||
const p = page === undefined ? 0 : page;
|
||||
const l = language === undefined ? null : language;
|
||||
let result;
|
||||
if (query === undefined) {
|
||||
result = await client.query(
|
||||
`
|
||||
await this.withClient(async (client) => {
|
||||
const pageSize = 20;
|
||||
const p = page === undefined ? 0 : page;
|
||||
const l = language === undefined ? null : language;
|
||||
let result;
|
||||
if (query === undefined) {
|
||||
result = await client.query(
|
||||
`
|
||||
SELECT id, name, author_id, author, language, calls, responses, public, version
|
||||
FROM manydecks.summaries
|
||||
WHERE summaries.public AND ($3::text IS NULL OR summaries.language = $3::text)
|
||||
ORDER BY id DESC
|
||||
OFFSET $1::int * $2::int LIMIT $2
|
||||
`,
|
||||
[p, pageSize, l]
|
||||
);
|
||||
} else {
|
||||
result = await client.query(
|
||||
`
|
||||
[p, pageSize, l]
|
||||
);
|
||||
} else {
|
||||
result = await client.query(
|
||||
`
|
||||
SELECT id, name, author_id, author, language, calls, responses, public, version, ts_rank_cd(deck_search , query) AS rank
|
||||
FROM manydecks.summaries, to_tsquery($4) query
|
||||
WHERE summaries.public AND ($3::text IS NULL OR summaries.language = $3::text)
|
||||
@@ -327,49 +339,49 @@ export class Store {
|
||||
ORDER BY rank DESC
|
||||
OFFSET $1::int * $2::int LIMIT $2
|
||||
`,
|
||||
[p, pageSize, l, query]
|
||||
);
|
||||
}
|
||||
return Store.summariesFromRows(result);
|
||||
});
|
||||
[p, pageSize, l, query]
|
||||
);
|
||||
}
|
||||
return Store.summariesFromRows(result);
|
||||
});
|
||||
|
||||
public updateDeck: (
|
||||
id: number,
|
||||
user: string,
|
||||
patch: Patch.Operation[]
|
||||
) => Promise<Deck.Deck> = async (id, user, patch) =>
|
||||
await this.withClient(async (client) => {
|
||||
const deck = await this.getDeck(id);
|
||||
try {
|
||||
JsonPatch.applyPatch(deck, patch);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof JsonPatch.JsonPatchError &&
|
||||
error.name === "TEST_OPERATION_FAILED"
|
||||
) {
|
||||
throw new Errors.PatchTestFailed();
|
||||
} else {
|
||||
throw new Errors.BadPatch();
|
||||
}
|
||||
}
|
||||
let updated;
|
||||
try {
|
||||
updated = Deck.validate({
|
||||
...deck,
|
||||
author: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
await this.withClient(async (client) => {
|
||||
const deck = await this.getDeck(id);
|
||||
try {
|
||||
JsonPatch.applyPatch(deck, patch);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof JsonPatch.JsonPatchError &&
|
||||
error.name === "TEST_OPERATION_FAILED"
|
||||
) {
|
||||
throw new Errors.PatchTestFailed();
|
||||
} else {
|
||||
throw new Errors.BadPatch();
|
||||
}
|
||||
await client.query(
|
||||
`
|
||||
}
|
||||
let updated;
|
||||
try {
|
||||
updated = Deck.validate({
|
||||
...deck,
|
||||
author: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Errors.BadPatch();
|
||||
}
|
||||
await client.query(
|
||||
`
|
||||
UPDATE manydecks.decks SET deck = $1 WHERE id = $2;
|
||||
`,
|
||||
[updated, id]
|
||||
);
|
||||
return deck;
|
||||
});
|
||||
[updated, id]
|
||||
);
|
||||
return deck;
|
||||
});
|
||||
|
||||
public deleteDeck: (id: number, user: string) => Promise<void> = async (
|
||||
id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["./src/ts/**/*"],
|
||||
"include": ["./src/ts/**/*", "./types/**/*"],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
@@ -19,6 +19,7 @@
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"typeRoots": ["./types"]
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// This is just a hack-y type def to stop compiler complaints, obviously not complete.
|
||||
|
||||
declare namespace Express {
|
||||
export interface Response {
|
||||
zip(options: {
|
||||
filename: string;
|
||||
files: { name: string; content: string }[];
|
||||
}): this;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "express-easy-zip" {
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
export const Zip: () => RequestHandler;
|
||||
|
||||
export default Zip;
|
||||
}
|
||||
Reference in New Issue
Block a user