diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/http')
-rw-r--r-- | Timeline/ClientApp/src/app/http/common.ts | 322 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/mock/common.ts | 156 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/mock/install.ts | 22 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/mock/timeline.ts | 1316 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/mock/token.ts | 106 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/mock/user.ts | 280 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/timeline.ts | 1086 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/token.ts | 144 | ||||
-rw-r--r-- | Timeline/ClientApp/src/app/http/user.ts | 268 |
9 files changed, 1850 insertions, 1850 deletions
diff --git a/Timeline/ClientApp/src/app/http/common.ts b/Timeline/ClientApp/src/app/http/common.ts index 1f904106..54203d1a 100644 --- a/Timeline/ClientApp/src/app/http/common.ts +++ b/Timeline/ClientApp/src/app/http/common.ts @@ -1,161 +1,161 @@ -import { AxiosError, AxiosResponse } from 'axios';
-
-export const apiBaseUrl = '/api';
-
-export function base64(blob: Blob): Promise<string> {
- return new Promise<string>((resolve) => {
- const reader = new FileReader();
- reader.onload = function () {
- resolve((reader.result as string).replace(/^data:.+;base64,/, ''));
- };
- reader.readAsDataURL(blob);
- });
-}
-
-export function extractStatusCode(error: AxiosError): number | null {
- if (error.isAxiosError) {
- const code = error?.response?.status;
- if (typeof code === 'number') {
- return code;
- }
- }
- return null;
-}
-
-export interface CommonErrorResponse {
- code: number;
- message: string;
-}
-
-export function extractErrorCode(
- error: AxiosError<CommonErrorResponse>
-): number | null {
- if (error.isAxiosError) {
- const code = error.response?.data?.code;
- if (typeof code === 'number') {
- return code;
- }
- }
- return null;
-}
-
-export class HttpNetworkError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class HttpForbiddenError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class NotModified {}
-
-export interface BlobWithEtag {
- data: Blob;
- etag: string;
-}
-
-export function extractResponseData<T>(res: AxiosResponse<T>): T {
- return res.data;
-}
-
-export function catchIfStatusCodeIs<
- TResult,
- TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined
->(
- statusCode: number,
- errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult
-): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult {
- return (error: AxiosError<CommonErrorResponse>) => {
- if (extractStatusCode(error) == statusCode) {
- return errorHandler(error);
- } else {
- throw error;
- }
- };
-}
-
-export function convertToIfStatusCodeIs<NewError>(
- statusCode: number,
- newErrorType: {
- new (innerError: AxiosError): NewError;
- }
-): (error: AxiosError<CommonErrorResponse>) => never {
- return catchIfStatusCodeIs(statusCode, (error) => {
- throw new newErrorType(error);
- });
-}
-
-export function catchIfErrorCodeIs<
- TResult,
- TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined
->(
- errorCode: number,
- errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult
-): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult {
- return (error: AxiosError<CommonErrorResponse>) => {
- if (extractErrorCode(error) == errorCode) {
- return errorHandler(error);
- } else {
- throw error;
- }
- };
-}
-export function convertToIfErrorCodeIs<NewError>(
- errorCode: number,
- newErrorType: {
- new (innerError: AxiosError): NewError;
- }
-): (error: AxiosError<CommonErrorResponse>) => never {
- return catchIfErrorCodeIs(errorCode, (error) => {
- throw new newErrorType(error);
- });
-}
-
-export function convertToNetworkError(
- error: AxiosError<CommonErrorResponse>
-): never {
- if (error.isAxiosError && error.response == null) {
- throw new HttpNetworkError(error);
- } else {
- throw error;
- }
-}
-
-export function convertToForbiddenError(
- error: AxiosError<CommonErrorResponse>
-): never {
- if (
- error.isAxiosError &&
- error.response != null &&
- (error.response.status == 401 || error.response.status == 403)
- ) {
- throw new HttpForbiddenError(error);
- } else {
- throw error;
- }
-}
-
-export function convertToNotModified(
- error: AxiosError<CommonErrorResponse>
-): NotModified {
- if (
- error.isAxiosError &&
- error.response != null &&
- error.response.status == 304
- ) {
- return new NotModified();
- } else {
- throw error;
- }
-}
-
-export function convertToBlobWithEtag(res: AxiosResponse<Blob>): BlobWithEtag {
- return {
- data: res.data,
- etag: (res.headers as Record<'etag', string>)['etag'],
- };
-}
+import { AxiosError, AxiosResponse } from "axios"; + +export const apiBaseUrl = "/api"; + +export function base64(blob: Blob): Promise<string> { + return new Promise<string>((resolve) => { + const reader = new FileReader(); + reader.onload = function () { + resolve((reader.result as string).replace(/^data:.+;base64,/, "")); + }; + reader.readAsDataURL(blob); + }); +} + +export function extractStatusCode(error: AxiosError): number | null { + if (error.isAxiosError) { + const code = error?.response?.status; + if (typeof code === "number") { + return code; + } + } + return null; +} + +export interface CommonErrorResponse { + code: number; + message: string; +} + +export function extractErrorCode( + error: AxiosError<CommonErrorResponse> +): number | null { + if (error.isAxiosError) { + const code = error.response?.data?.code; + if (typeof code === "number") { + return code; + } + } + return null; +} + +export class HttpNetworkError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpForbiddenError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class NotModified {} + +export interface BlobWithEtag { + data: Blob; + etag: string; +} + +export function extractResponseData<T>(res: AxiosResponse<T>): T { + return res.data; +} + +export function catchIfStatusCodeIs< + TResult, + TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined +>( + statusCode: number, + errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult +): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult { + return (error: AxiosError<CommonErrorResponse>) => { + if (extractStatusCode(error) == statusCode) { + return errorHandler(error); + } else { + throw error; + } + }; +} + +export function convertToIfStatusCodeIs<NewError>( + statusCode: number, + newErrorType: { + new (innerError: AxiosError): NewError; + } +): (error: AxiosError<CommonErrorResponse>) => never { + return catchIfStatusCodeIs(statusCode, (error) => { + throw new newErrorType(error); + }); +} + +export function catchIfErrorCodeIs< + TResult, + TErrorHandlerResult extends TResult | PromiseLike<TResult> | null | undefined +>( + errorCode: number, + errorHandler: (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult +): (error: AxiosError<CommonErrorResponse>) => TErrorHandlerResult { + return (error: AxiosError<CommonErrorResponse>) => { + if (extractErrorCode(error) == errorCode) { + return errorHandler(error); + } else { + throw error; + } + }; +} +export function convertToIfErrorCodeIs<NewError>( + errorCode: number, + newErrorType: { + new (innerError: AxiosError): NewError; + } +): (error: AxiosError<CommonErrorResponse>) => never { + return catchIfErrorCodeIs(errorCode, (error) => { + throw new newErrorType(error); + }); +} + +export function convertToNetworkError( + error: AxiosError<CommonErrorResponse> +): never { + if (error.isAxiosError && error.response == null) { + throw new HttpNetworkError(error); + } else { + throw error; + } +} + +export function convertToForbiddenError( + error: AxiosError<CommonErrorResponse> +): never { + if ( + error.isAxiosError && + error.response != null && + (error.response.status == 401 || error.response.status == 403) + ) { + throw new HttpForbiddenError(error); + } else { + throw error; + } +} + +export function convertToNotModified( + error: AxiosError<CommonErrorResponse> +): NotModified { + if ( + error.isAxiosError && + error.response != null && + error.response.status == 304 + ) { + return new NotModified(); + } else { + throw error; + } +} + +export function convertToBlobWithEtag(res: AxiosResponse<Blob>): BlobWithEtag { + return { + data: res.data, + etag: (res.headers as Record<"etag", string>)["etag"], + }; +} diff --git a/Timeline/ClientApp/src/app/http/mock/common.ts b/Timeline/ClientApp/src/app/http/mock/common.ts index 3dce8117..787d81bd 100644 --- a/Timeline/ClientApp/src/app/http/mock/common.ts +++ b/Timeline/ClientApp/src/app/http/mock/common.ts @@ -1,78 +1,78 @@ -import localforage from 'localforage';
-import { SHA1 } from 'crypto-js';
-
-import { HttpNetworkError } from '../common';
-
-export const mockStorage = localforage.createInstance({
- name: 'mock-backend',
- description: 'Database for mock back end.',
- driver: localforage.INDEXEDDB,
-});
-
-export async function sha1(data: Blob): Promise<string> {
- const s = await new Promise<string>((resolve) => {
- const fileReader = new FileReader();
- fileReader.readAsBinaryString(data);
- fileReader.onload = () => {
- resolve(fileReader.result as string);
- };
- });
-
- return SHA1(s).toString();
-}
-
-const disableNetworkKey = 'mockServer.disableNetwork';
-const networkLatencyKey = 'mockServer.networkLatency';
-
-let disableNetwork: boolean =
- localStorage.getItem(disableNetworkKey) === 'true' ? true : false;
-
-const savedNetworkLatency = localStorage.getItem(networkLatencyKey);
-
-let networkLatency: number | null =
- savedNetworkLatency != null ? Number(savedNetworkLatency) : null;
-
-Object.defineProperty(window, 'disableNetwork', {
- get: () => disableNetwork,
- set: (value) => {
- if (value) {
- disableNetwork = true;
- localStorage.setItem(disableNetworkKey, 'true');
- } else {
- disableNetwork = false;
- localStorage.setItem(disableNetworkKey, 'false');
- }
- },
-});
-
-Object.defineProperty(window, 'networkLatency', {
- get: () => networkLatency,
- set: (value) => {
- if (typeof value === 'number') {
- networkLatency = value;
- localStorage.setItem(networkLatencyKey, value.toString());
- } else if (value == null) {
- networkLatency = null;
- localStorage.removeItem(networkLatencyKey);
- }
- },
-});
-
-export async function mockPrepare(key: string): Promise<void> {
- console.log(`Recieve request: ${key}`);
-
- if (disableNetwork) {
- console.warn('Network is disabled for mock server.');
- throw new HttpNetworkError();
- }
- if (networkLatency != null) {
- await new Promise((resolve) => {
- window.setTimeout(() => {
- resolve();
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- }, networkLatency! * 1000);
- });
- }
-
- await Promise.resolve();
-}
+import localforage from "localforage"; +import { SHA1 } from "crypto-js"; + +import { HttpNetworkError } from "../common"; + +export const mockStorage = localforage.createInstance({ + name: "mock-backend", + description: "Database for mock back end.", + driver: localforage.INDEXEDDB, +}); + +export async function sha1(data: Blob): Promise<string> { + const s = await new Promise<string>((resolve) => { + const fileReader = new FileReader(); + fileReader.readAsBinaryString(data); + fileReader.onload = () => { + resolve(fileReader.result as string); + }; + }); + + return SHA1(s).toString(); +} + +const disableNetworkKey = "mockServer.disableNetwork"; +const networkLatencyKey = "mockServer.networkLatency"; + +let disableNetwork: boolean = + localStorage.getItem(disableNetworkKey) === "true" ? true : false; + +const savedNetworkLatency = localStorage.getItem(networkLatencyKey); + +let networkLatency: number | null = + savedNetworkLatency != null ? Number(savedNetworkLatency) : null; + +Object.defineProperty(window, "disableNetwork", { + get: () => disableNetwork, + set: (value) => { + if (value) { + disableNetwork = true; + localStorage.setItem(disableNetworkKey, "true"); + } else { + disableNetwork = false; + localStorage.setItem(disableNetworkKey, "false"); + } + }, +}); + +Object.defineProperty(window, "networkLatency", { + get: () => networkLatency, + set: (value) => { + if (typeof value === "number") { + networkLatency = value; + localStorage.setItem(networkLatencyKey, value.toString()); + } else if (value == null) { + networkLatency = null; + localStorage.removeItem(networkLatencyKey); + } + }, +}); + +export async function mockPrepare(key: string): Promise<void> { + console.log(`Recieve request: ${key}`); + + if (disableNetwork) { + console.warn("Network is disabled for mock server."); + throw new HttpNetworkError(); + } + if (networkLatency != null) { + await new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + }, networkLatency! * 1000); + }); + } + + await Promise.resolve(); +} diff --git a/Timeline/ClientApp/src/app/http/mock/install.ts b/Timeline/ClientApp/src/app/http/mock/install.ts index 66174d41..17b7cc13 100644 --- a/Timeline/ClientApp/src/app/http/mock/install.ts +++ b/Timeline/ClientApp/src/app/http/mock/install.ts @@ -1,11 +1,11 @@ -import { setHttpTokenClient } from '../token';
-import { setHttpUserClient } from '../user';
-import { setHttpTimelineClient } from '../timeline';
-
-import { MockHttpTokenClient } from './token';
-import { MockHttpUserClient } from './user';
-import { MockHttpTimelineClient } from './timeline';
-
-setHttpTokenClient(new MockHttpTokenClient());
-setHttpUserClient(new MockHttpUserClient());
-setHttpTimelineClient(new MockHttpTimelineClient());
+import { setHttpTokenClient } from "../token"; +import { setHttpUserClient } from "../user"; +import { setHttpTimelineClient } from "../timeline"; + +import { MockHttpTokenClient } from "./token"; +import { MockHttpUserClient } from "./user"; +import { MockHttpTimelineClient } from "./timeline"; + +setHttpTokenClient(new MockHttpTokenClient()); +setHttpUserClient(new MockHttpUserClient()); +setHttpTimelineClient(new MockHttpTimelineClient()); diff --git a/Timeline/ClientApp/src/app/http/mock/timeline.ts b/Timeline/ClientApp/src/app/http/mock/timeline.ts index 9434f666..0330b303 100644 --- a/Timeline/ClientApp/src/app/http/mock/timeline.ts +++ b/Timeline/ClientApp/src/app/http/mock/timeline.ts @@ -1,658 +1,658 @@ -import { random, without, range } from 'lodash';
-
-import { BlobWithEtag, NotModified } from '../common';
-import {
- IHttpTimelineClient,
- HttpTimelineInfo,
- TimelineVisibility,
- HttpTimelineListQuery,
- HttpTimelineNotExistError,
- HttpTimelinePostRequest,
- HttpTimelineNameConflictError,
- HttpTimelinePatchRequest,
- HttpTimelinePostInfo,
- HttpTimelinePostContent,
- HttpTimelinePostPostRequest,
- HttpTimelinePostNotExistError,
- HttpTimelineGenericPostInfo,
-} from '../timeline';
-import { HttpUser } from '../user';
-
-import { mockStorage, sha1, mockPrepare } from './common';
-import { getUser, MockUserNotExistError, checkToken } from './user';
-
-async function getTimelineNameList(): Promise<string[]> {
- return (await mockStorage.getItem<string[]>('timelines')) ?? [];
-}
-
-async function setTimelineNameList(newOne: string[]): Promise<void> {
- await mockStorage.setItem<string[]>('timelines', newOne);
-}
-
-type TimelinePropertyKey =
- | 'uniqueId'
- | 'lastModified'
- | 'owner'
- | 'description'
- | 'visibility'
- | 'members'
- | 'currentPostId';
-
-function getTimelinePropertyKey(
- name: string,
- property: TimelinePropertyKey
-): string {
- return `timeline.${name}.${property}`;
-}
-
-function getTimelinePropertyValue<T>(
- name: string,
- property: TimelinePropertyKey
-): Promise<T> {
- return mockStorage.getItem<T>(getTimelinePropertyKey(name, property));
-}
-
-function setTimelinePropertyValue<T>(
- name: string,
- property: TimelinePropertyKey,
- value: T
-): Promise<void> {
- return mockStorage
- .setItem<T>(getTimelinePropertyKey(name, property), value)
- .then();
-}
-
-function updateTimelineLastModified(name: string): Promise<void> {
- return setTimelinePropertyValue(
- name,
- 'lastModified',
- new Date().toISOString()
- );
-}
-
-interface HttpTimelineInfoEx extends HttpTimelineInfo {
- memberUsernames: string[];
-}
-
-function createUniqueId(): string {
- const s = 'abcdefghijklmnopqrstuvwxz0123456789';
- let result = '';
- for (let i = 0; i < 16; i++) {
- result += s[random(0, s.length - 1)];
- }
- return result;
-}
-
-class MockTimelineNotExistError extends Error {
- constructor() {
- super('Timeline not exist.');
- }
-}
-
-class MockTimelineAlreadyExistError extends Error {
- constructor() {
- super('Timeline already exist.');
- }
-}
-
-async function getTimelineInfo(name: string): Promise<HttpTimelineInfoEx> {
- let owner: HttpUser;
- if (name.startsWith('@')) {
- const ownerUsername = name.substr(1);
- owner = await getUser(ownerUsername);
- const optionalUniqueId = await getTimelinePropertyValue<string | null>(
- name,
- 'uniqueId'
- );
- if (optionalUniqueId == null) {
- await setTimelineNameList([...(await getTimelineNameList()), name]);
- await setTimelinePropertyValue(name, 'uniqueId', createUniqueId());
- await updateTimelineLastModified(name);
- }
- } else {
- const optionalOwnerUsername = await getTimelinePropertyValue<string | null>(
- name,
- 'owner'
- );
- if (optionalOwnerUsername == null) {
- throw new MockTimelineNotExistError();
- } else {
- owner = await getUser(optionalOwnerUsername);
- }
- }
-
- const memberUsernames =
- (await getTimelinePropertyValue<string[] | null>(name, 'members')) ?? [];
- const members = await Promise.all(
- memberUsernames.map(async (username) => {
- return await getUser(username);
- })
- );
-
- return {
- name,
- uniqueId: await getTimelinePropertyValue<string>(name, 'uniqueId'),
- owner,
- description:
- (await getTimelinePropertyValue<string | null>(name, 'description')) ??
- '',
- visibility:
- (await getTimelinePropertyValue<TimelineVisibility | null>(
- name,
- 'visibility'
- )) ?? 'Register',
- lastModified: new Date(
- await getTimelinePropertyValue<string>(name, 'lastModified')
- ),
- members,
- memberUsernames,
- };
-}
-
-async function createTimeline(name: string, owner: string): Promise<void> {
- const optionalOwnerUsername = await getTimelinePropertyValue<string | null>(
- name,
- 'owner'
- );
- if (optionalOwnerUsername != null) {
- throw new MockTimelineAlreadyExistError();
- }
-
- await setTimelineNameList([...(await getTimelineNameList()), name]);
- await setTimelinePropertyValue(name, 'uniqueId', createUniqueId());
- await setTimelinePropertyValue(name, 'owner', owner);
- await updateTimelineLastModified(name);
-}
-
-type TimelinePostPropertyKey =
- | 'type'
- | 'data'
- | 'etag'
- | 'author'
- | 'time'
- | 'lastUpdated';
-
-function getTimelinePostPropertyKey(
- timelineName: string,
- id: number,
- propertyKey: TimelinePostPropertyKey
-): string {
- return `timeline.${timelineName}.posts.${id}.${propertyKey}`;
-}
-
-function getTimelinePostPropertyValue<T>(
- timelineName: string,
- id: number,
- propertyKey: TimelinePostPropertyKey
-): Promise<T> {
- return mockStorage.getItem<T>(
- getTimelinePostPropertyKey(timelineName, id, propertyKey)
- );
-}
-
-function setTimelinePostPropertyValue<T>(
- timelineName: string,
- id: number,
- propertyKey: TimelinePostPropertyKey,
- value: T
-): Promise<T> {
- return mockStorage.setItem(
- getTimelinePostPropertyKey(timelineName, id, propertyKey),
- value
- );
-}
-
-function removeTimelinePostProperty(
- timelineName: string,
- id: number,
- propertyKey: TimelinePostPropertyKey
-): Promise<void> {
- return mockStorage.removeItem(
- getTimelinePostPropertyKey(timelineName, id, propertyKey)
- );
-}
-
-async function getTimelinePostInfo(
- timelineName: string,
- id: number
-): Promise<HttpTimelineGenericPostInfo> {
- const currentPostId = await getTimelinePropertyValue<number | null>(
- timelineName,
- 'currentPostId'
- );
- if (currentPostId == null || id > currentPostId) {
- throw new HttpTimelinePostNotExistError();
- }
-
- const type = await getTimelinePostPropertyValue<string | null>(
- timelineName,
- id,
- 'type'
- );
-
- if (type == null) {
- return {
- id,
- author: await getUser(
- await getTimelinePostPropertyValue<string>(timelineName, id, 'author')
- ),
- time: new Date(
- await getTimelinePostPropertyValue<string>(timelineName, id, 'time')
- ),
- lastUpdated: new Date(
- await getTimelinePostPropertyValue<string>(
- timelineName,
- id,
- 'lastUpdated'
- )
- ),
- deleted: true,
- };
- } else {
- let content: HttpTimelinePostContent;
- if (type === 'text') {
- content = {
- type: 'text',
- text: await getTimelinePostPropertyValue(timelineName, id, 'data'),
- };
- } else {
- content = {
- type: 'image',
- };
- }
-
- return {
- id,
- author: await getUser(
- await getTimelinePostPropertyValue<string>(timelineName, id, 'author')
- ),
- time: new Date(
- await getTimelinePostPropertyValue<string>(timelineName, id, 'time')
- ),
- lastUpdated: new Date(
- await getTimelinePostPropertyValue<string>(
- timelineName,
- id,
- 'lastUpdated'
- )
- ),
- content,
- deleted: false,
- };
- }
-}
-
-export class MockHttpTimelineClient implements IHttpTimelineClient {
- async listTimeline(
- query: HttpTimelineListQuery
- ): Promise<HttpTimelineInfo[]> {
- await mockPrepare('timeline.list');
- return (
- await Promise.all(
- (await getTimelineNameList()).map((name) => getTimelineInfo(name))
- )
- ).filter((timeline) => {
- if (
- query.visibility != null &&
- query.visibility !== timeline.visibility
- ) {
- return false;
- }
- if (query.relate != null) {
- if (query.relateType === 'own') {
- if (timeline.owner.username !== query.relate) {
- return false;
- }
- } else if (query.relateType === 'join') {
- if (!timeline.memberUsernames.includes(query.relate)) {
- return false;
- }
- } else if (
- timeline.owner.username !== query.relate &&
- !timeline.memberUsernames.includes(query.relate)
- ) {
- return false;
- }
- }
- return true;
- });
- }
-
- getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- }
- ): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- ifModifiedSince: Date;
- }
- ): Promise<HttpTimelineInfo | NotModified>;
- async getTimeline(
- timelineName: string,
- query?: {
- checkUniqueId?: string;
- ifModifiedSince?: Date;
- }
- ): Promise<HttpTimelineInfo | NotModified> {
- await mockPrepare('timeline.get');
- try {
- const timeline = await getTimelineInfo(timelineName);
- if (query != null && query.ifModifiedSince != null) {
- if (timeline.lastModified >= query.ifModifiedSince) {
- return timeline;
- } else {
- if (
- query.checkUniqueId != null &&
- timeline.uniqueId != query.checkUniqueId
- ) {
- return timeline;
- } else {
- return new NotModified();
- }
- }
- }
-
- return timeline;
- } catch (e) {
- if (
- e instanceof MockTimelineNotExistError ||
- e instanceof MockUserNotExistError
- ) {
- throw new HttpTimelineNotExistError();
- }
- throw e;
- }
- }
-
- async postTimeline(
- req: HttpTimelinePostRequest,
- token: string
- ): Promise<HttpTimelineInfo> {
- await mockPrepare('timeline.post');
- const user = checkToken(token);
- try {
- await createTimeline(req.name, user);
- } catch (e) {
- if (e instanceof MockTimelineAlreadyExistError) {
- throw new HttpTimelineNameConflictError();
- }
- throw e;
- }
- return await getTimelineInfo(req.name);
- }
-
- async patchTimeline(
- timelineName: string,
- req: HttpTimelinePatchRequest,
- _token: string
- ): Promise<HttpTimelineInfo> {
- await mockPrepare('timeline.patch');
- let modified = false;
- if (req.description != null) {
- modified = true;
- await setTimelinePropertyValue(
- timelineName,
- 'description',
- req.description
- );
- }
- if (req.visibility != null) {
- modified = true;
- await setTimelinePropertyValue(
- timelineName,
- 'visibility',
- req.visibility
- );
- }
- if (modified) {
- await updateTimelineLastModified(timelineName);
- }
- return await getTimelineInfo(timelineName);
- }
-
- async deleteTimeline(timelineName: string, _token: string): Promise<void> {
- await mockPrepare('timeline.delete');
- await setTimelineNameList(
- without(await getTimelineNameList(), timelineName)
- );
- await mockStorage.removeItem(
- getTimelinePropertyKey(timelineName, 'uniqueId')
- );
-
- // TODO: remove other things
- }
-
- async memberPut(
- timelineName: string,
- username: string,
- _token: string
- ): Promise<void> {
- await mockPrepare('timeline.member.put');
- const oldMembers =
- (await getTimelinePropertyValue<string[] | null>(
- timelineName,
- 'members'
- )) ?? [];
- if (!oldMembers.includes(username)) {
- await setTimelinePropertyValue(timelineName, 'members', [
- ...oldMembers,
- username,
- ]);
- await updateTimelineLastModified(timelineName);
- }
- }
-
- async memberDelete(
- timelineName: string,
- username: string,
- _token: string
- ): Promise<void> {
- await mockPrepare('timeline.member.delete');
- const oldMembers =
- (await getTimelinePropertyValue<string[] | null>(
- timelineName,
- 'members'
- )) ?? [];
- if (oldMembers.includes(username)) {
- await setTimelinePropertyValue(
- timelineName,
- 'members',
- without(oldMembers, username)
- );
- await updateTimelineLastModified(timelineName);
- }
- }
-
- listPost(
- timelineName: string,
- token?: string
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted?: false;
- }
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted: true;
- }
- ): Promise<HttpTimelineGenericPostInfo[]>;
- async listPost(
- timelineName: string,
- _token?: string,
- query?: {
- modifiedSince?: Date;
- includeDeleted?: boolean;
- }
- ): Promise<HttpTimelineGenericPostInfo[]> {
- await mockPrepare('timeline.post.list');
- // TODO: Permission check.
-
- const currentPostId = await getTimelinePropertyValue<number | null>(
- timelineName,
- 'currentPostId'
- );
-
- return (
- await Promise.all(
- range(1, currentPostId == null ? 1 : currentPostId + 1).map(
- async (id) => {
- return await getTimelinePostInfo(timelineName, id);
- }
- )
- )
- )
- .filter((post) => {
- if (query?.includeDeleted !== true && post.deleted) {
- return false;
- }
- return true;
- })
- .filter((post) => {
- if (query?.modifiedSince != null) {
- return post.lastUpdated >= query.modifiedSince;
- }
- return true;
- });
- }
-
- getPostData(
- timelineName: string,
- postId: number,
- token: string
- ): Promise<BlobWithEtag>;
- async getPostData(
- timelineName: string,
- postId: number,
- _token?: string,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- await mockPrepare('timeline.post.data.get');
- // TODO: Permission check.
-
- const optionalSavedEtag = await getTimelinePostPropertyValue<string>(
- timelineName,
- postId,
- 'etag'
- );
-
- if (optionalSavedEtag == null) {
- const optionalType = await getTimelinePostPropertyValue<string>(
- timelineName,
- postId,
- 'type'
- );
-
- if (optionalType != null) {
- throw new Error('Post of this type has no data.');
- } else {
- throw new HttpTimelinePostNotExistError();
- }
- }
-
- if (etag === optionalSavedEtag) {
- return new NotModified();
- }
-
- return {
- data: await getTimelinePostPropertyValue<Blob>(
- timelineName,
- postId,
- 'data'
- ),
- etag: optionalSavedEtag,
- };
- }
-
- async postPost(
- timelineName: string,
- req: HttpTimelinePostPostRequest,
- token: string
- ): Promise<HttpTimelinePostInfo> {
- await mockPrepare('timeline.post.post');
- const user = checkToken(token);
-
- const savedId = await getTimelinePropertyValue<number | null>(
- timelineName,
- 'currentPostId'
- );
- const id = savedId ? savedId + 1 : 1;
- await setTimelinePropertyValue(timelineName, 'currentPostId', id);
-
- await setTimelinePostPropertyValue(timelineName, id, 'author', user);
-
- const currentTimeString = new Date().toISOString();
- await setTimelinePostPropertyValue(
- timelineName,
- id,
- 'lastUpdated',
- currentTimeString
- );
-
- await setTimelinePostPropertyValue(
- timelineName,
- id,
- 'time',
- req.time != null ? req.time.toISOString() : currentTimeString
- );
-
- const { content } = req;
- if (content.type === 'text') {
- await setTimelinePostPropertyValue(timelineName, id, 'type', 'text');
- await setTimelinePostPropertyValue(
- timelineName,
- id,
- 'data',
- content.text
- );
- } else {
- await setTimelinePostPropertyValue(timelineName, id, 'type', 'image');
- await setTimelinePostPropertyValue(
- timelineName,
- id,
- 'data',
- content.data
- );
- await setTimelinePostPropertyValue(
- timelineName,
- id,
- 'etag',
- await sha1(content.data)
- );
- }
-
- return (await getTimelinePostInfo(
- timelineName,
- id
- )) as HttpTimelinePostInfo;
- }
-
- async deletePost(
- timelineName: string,
- postId: number,
- _token: string
- ): Promise<void> {
- await mockPrepare('timeline.post.delete');
- // TODO: permission check
- await removeTimelinePostProperty(timelineName, postId, 'type');
- await removeTimelinePostProperty(timelineName, postId, 'data');
- await removeTimelinePostProperty(timelineName, postId, 'etag');
- await setTimelinePostPropertyValue(
- timelineName,
- postId,
- 'lastUpdated',
- new Date().toISOString()
- );
- }
-}
+import { random, without, range } from "lodash"; + +import { BlobWithEtag, NotModified } from "../common"; +import { + IHttpTimelineClient, + HttpTimelineInfo, + TimelineVisibility, + HttpTimelineListQuery, + HttpTimelineNotExistError, + HttpTimelinePostRequest, + HttpTimelineNameConflictError, + HttpTimelinePatchRequest, + HttpTimelinePostInfo, + HttpTimelinePostContent, + HttpTimelinePostPostRequest, + HttpTimelinePostNotExistError, + HttpTimelineGenericPostInfo, +} from "../timeline"; +import { HttpUser } from "../user"; + +import { mockStorage, sha1, mockPrepare } from "./common"; +import { getUser, MockUserNotExistError, checkToken } from "./user"; + +async function getTimelineNameList(): Promise<string[]> { + return (await mockStorage.getItem<string[]>("timelines")) ?? []; +} + +async function setTimelineNameList(newOne: string[]): Promise<void> { + await mockStorage.setItem<string[]>("timelines", newOne); +} + +type TimelinePropertyKey = + | "uniqueId" + | "lastModified" + | "owner" + | "description" + | "visibility" + | "members" + | "currentPostId"; + +function getTimelinePropertyKey( + name: string, + property: TimelinePropertyKey +): string { + return `timeline.${name}.${property}`; +} + +function getTimelinePropertyValue<T>( + name: string, + property: TimelinePropertyKey +): Promise<T> { + return mockStorage.getItem<T>(getTimelinePropertyKey(name, property)); +} + +function setTimelinePropertyValue<T>( + name: string, + property: TimelinePropertyKey, + value: T +): Promise<void> { + return mockStorage + .setItem<T>(getTimelinePropertyKey(name, property), value) + .then(); +} + +function updateTimelineLastModified(name: string): Promise<void> { + return setTimelinePropertyValue( + name, + "lastModified", + new Date().toISOString() + ); +} + +interface HttpTimelineInfoEx extends HttpTimelineInfo { + memberUsernames: string[]; +} + +function createUniqueId(): string { + const s = "abcdefghijklmnopqrstuvwxz0123456789"; + let result = ""; + for (let i = 0; i < 16; i++) { + result += s[random(0, s.length - 1)]; + } + return result; +} + +class MockTimelineNotExistError extends Error { + constructor() { + super("Timeline not exist."); + } +} + +class MockTimelineAlreadyExistError extends Error { + constructor() { + super("Timeline already exist."); + } +} + +async function getTimelineInfo(name: string): Promise<HttpTimelineInfoEx> { + let owner: HttpUser; + if (name.startsWith("@")) { + const ownerUsername = name.substr(1); + owner = await getUser(ownerUsername); + const optionalUniqueId = await getTimelinePropertyValue<string | null>( + name, + "uniqueId" + ); + if (optionalUniqueId == null) { + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); + await updateTimelineLastModified(name); + } + } else { + const optionalOwnerUsername = await getTimelinePropertyValue<string | null>( + name, + "owner" + ); + if (optionalOwnerUsername == null) { + throw new MockTimelineNotExistError(); + } else { + owner = await getUser(optionalOwnerUsername); + } + } + + const memberUsernames = + (await getTimelinePropertyValue<string[] | null>(name, "members")) ?? []; + const members = await Promise.all( + memberUsernames.map(async (username) => { + return await getUser(username); + }) + ); + + return { + name, + uniqueId: await getTimelinePropertyValue<string>(name, "uniqueId"), + owner, + description: + (await getTimelinePropertyValue<string | null>(name, "description")) ?? + "", + visibility: + (await getTimelinePropertyValue<TimelineVisibility | null>( + name, + "visibility" + )) ?? "Register", + lastModified: new Date( + await getTimelinePropertyValue<string>(name, "lastModified") + ), + members, + memberUsernames, + }; +} + +async function createTimeline(name: string, owner: string): Promise<void> { + const optionalOwnerUsername = await getTimelinePropertyValue<string | null>( + name, + "owner" + ); + if (optionalOwnerUsername != null) { + throw new MockTimelineAlreadyExistError(); + } + + await setTimelineNameList([...(await getTimelineNameList()), name]); + await setTimelinePropertyValue(name, "uniqueId", createUniqueId()); + await setTimelinePropertyValue(name, "owner", owner); + await updateTimelineLastModified(name); +} + +type TimelinePostPropertyKey = + | "type" + | "data" + | "etag" + | "author" + | "time" + | "lastUpdated"; + +function getTimelinePostPropertyKey( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): string { + return `timeline.${timelineName}.posts.${id}.${propertyKey}`; +} + +function getTimelinePostPropertyValue<T>( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise<T> { + return mockStorage.getItem<T>( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +function setTimelinePostPropertyValue<T>( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey, + value: T +): Promise<T> { + return mockStorage.setItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey), + value + ); +} + +function removeTimelinePostProperty( + timelineName: string, + id: number, + propertyKey: TimelinePostPropertyKey +): Promise<void> { + return mockStorage.removeItem( + getTimelinePostPropertyKey(timelineName, id, propertyKey) + ); +} + +async function getTimelinePostInfo( + timelineName: string, + id: number +): Promise<HttpTimelineGenericPostInfo> { + const currentPostId = await getTimelinePropertyValue<number | null>( + timelineName, + "currentPostId" + ); + if (currentPostId == null || id > currentPostId) { + throw new HttpTimelinePostNotExistError(); + } + + const type = await getTimelinePostPropertyValue<string | null>( + timelineName, + id, + "type" + ); + + if (type == null) { + return { + id, + author: await getUser( + await getTimelinePostPropertyValue<string>(timelineName, id, "author") + ), + time: new Date( + await getTimelinePostPropertyValue<string>(timelineName, id, "time") + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue<string>( + timelineName, + id, + "lastUpdated" + ) + ), + deleted: true, + }; + } else { + let content: HttpTimelinePostContent; + if (type === "text") { + content = { + type: "text", + text: await getTimelinePostPropertyValue(timelineName, id, "data"), + }; + } else { + content = { + type: "image", + }; + } + + return { + id, + author: await getUser( + await getTimelinePostPropertyValue<string>(timelineName, id, "author") + ), + time: new Date( + await getTimelinePostPropertyValue<string>(timelineName, id, "time") + ), + lastUpdated: new Date( + await getTimelinePostPropertyValue<string>( + timelineName, + id, + "lastUpdated" + ) + ), + content, + deleted: false, + }; + } +} + +export class MockHttpTimelineClient implements IHttpTimelineClient { + async listTimeline( + query: HttpTimelineListQuery + ): Promise<HttpTimelineInfo[]> { + await mockPrepare("timeline.list"); + return ( + await Promise.all( + (await getTimelineNameList()).map((name) => getTimelineInfo(name)) + ) + ).filter((timeline) => { + if ( + query.visibility != null && + query.visibility !== timeline.visibility + ) { + return false; + } + if (query.relate != null) { + if (query.relateType === "own") { + if (timeline.owner.username !== query.relate) { + return false; + } + } else if (query.relateType === "join") { + if (!timeline.memberUsernames.includes(query.relate)) { + return false; + } + } else if ( + timeline.owner.username !== query.relate && + !timeline.memberUsernames.includes(query.relate) + ) { + return false; + } + } + return true; + }); + } + + getTimeline(timelineName: string): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise<HttpTimelineInfo | NotModified>; + async getTimeline( + timelineName: string, + query?: { + checkUniqueId?: string; + ifModifiedSince?: Date; + } + ): Promise<HttpTimelineInfo | NotModified> { + await mockPrepare("timeline.get"); + try { + const timeline = await getTimelineInfo(timelineName); + if (query != null && query.ifModifiedSince != null) { + if (timeline.lastModified >= query.ifModifiedSince) { + return timeline; + } else { + if ( + query.checkUniqueId != null && + timeline.uniqueId != query.checkUniqueId + ) { + return timeline; + } else { + return new NotModified(); + } + } + } + + return timeline; + } catch (e) { + if ( + e instanceof MockTimelineNotExistError || + e instanceof MockUserNotExistError + ) { + throw new HttpTimelineNotExistError(); + } + throw e; + } + } + + async postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise<HttpTimelineInfo> { + await mockPrepare("timeline.post"); + const user = checkToken(token); + try { + await createTimeline(req.name, user); + } catch (e) { + if (e instanceof MockTimelineAlreadyExistError) { + throw new HttpTimelineNameConflictError(); + } + throw e; + } + return await getTimelineInfo(req.name); + } + + async patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + _token: string + ): Promise<HttpTimelineInfo> { + await mockPrepare("timeline.patch"); + let modified = false; + if (req.description != null) { + modified = true; + await setTimelinePropertyValue( + timelineName, + "description", + req.description + ); + } + if (req.visibility != null) { + modified = true; + await setTimelinePropertyValue( + timelineName, + "visibility", + req.visibility + ); + } + if (modified) { + await updateTimelineLastModified(timelineName); + } + return await getTimelineInfo(timelineName); + } + + async deleteTimeline(timelineName: string, _token: string): Promise<void> { + await mockPrepare("timeline.delete"); + await setTimelineNameList( + without(await getTimelineNameList(), timelineName) + ); + await mockStorage.removeItem( + getTimelinePropertyKey(timelineName, "uniqueId") + ); + + // TODO: remove other things + } + + async memberPut( + timelineName: string, + username: string, + _token: string + ): Promise<void> { + await mockPrepare("timeline.member.put"); + const oldMembers = + (await getTimelinePropertyValue<string[] | null>( + timelineName, + "members" + )) ?? []; + if (!oldMembers.includes(username)) { + await setTimelinePropertyValue(timelineName, "members", [ + ...oldMembers, + username, + ]); + await updateTimelineLastModified(timelineName); + } + } + + async memberDelete( + timelineName: string, + username: string, + _token: string + ): Promise<void> { + await mockPrepare("timeline.member.delete"); + const oldMembers = + (await getTimelinePropertyValue<string[] | null>( + timelineName, + "members" + )) ?? []; + if (oldMembers.includes(username)) { + await setTimelinePropertyValue( + timelineName, + "members", + without(oldMembers, username) + ); + await updateTimelineLastModified(timelineName); + } + } + + listPost( + timelineName: string, + token?: string + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise<HttpTimelineGenericPostInfo[]>; + async listPost( + timelineName: string, + _token?: string, + query?: { + modifiedSince?: Date; + includeDeleted?: boolean; + } + ): Promise<HttpTimelineGenericPostInfo[]> { + await mockPrepare("timeline.post.list"); + // TODO: Permission check. + + const currentPostId = await getTimelinePropertyValue<number | null>( + timelineName, + "currentPostId" + ); + + return ( + await Promise.all( + range(1, currentPostId == null ? 1 : currentPostId + 1).map( + async (id) => { + return await getTimelinePostInfo(timelineName, id); + } + ) + ) + ) + .filter((post) => { + if (query?.includeDeleted !== true && post.deleted) { + return false; + } + return true; + }) + .filter((post) => { + if (query?.modifiedSince != null) { + return post.lastUpdated >= query.modifiedSince; + } + return true; + }); + } + + getPostData( + timelineName: string, + postId: number, + token: string + ): Promise<BlobWithEtag>; + async getPostData( + timelineName: string, + postId: number, + _token?: string, + etag?: string + ): Promise<BlobWithEtag | NotModified> { + await mockPrepare("timeline.post.data.get"); + // TODO: Permission check. + + const optionalSavedEtag = await getTimelinePostPropertyValue<string>( + timelineName, + postId, + "etag" + ); + + if (optionalSavedEtag == null) { + const optionalType = await getTimelinePostPropertyValue<string>( + timelineName, + postId, + "type" + ); + + if (optionalType != null) { + throw new Error("Post of this type has no data."); + } else { + throw new HttpTimelinePostNotExistError(); + } + } + + if (etag === optionalSavedEtag) { + return new NotModified(); + } + + return { + data: await getTimelinePostPropertyValue<Blob>( + timelineName, + postId, + "data" + ), + etag: optionalSavedEtag, + }; + } + + async postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise<HttpTimelinePostInfo> { + await mockPrepare("timeline.post.post"); + const user = checkToken(token); + + const savedId = await getTimelinePropertyValue<number | null>( + timelineName, + "currentPostId" + ); + const id = savedId ? savedId + 1 : 1; + await setTimelinePropertyValue(timelineName, "currentPostId", id); + + await setTimelinePostPropertyValue(timelineName, id, "author", user); + + const currentTimeString = new Date().toISOString(); + await setTimelinePostPropertyValue( + timelineName, + id, + "lastUpdated", + currentTimeString + ); + + await setTimelinePostPropertyValue( + timelineName, + id, + "time", + req.time != null ? req.time.toISOString() : currentTimeString + ); + + const { content } = req; + if (content.type === "text") { + await setTimelinePostPropertyValue(timelineName, id, "type", "text"); + await setTimelinePostPropertyValue( + timelineName, + id, + "data", + content.text + ); + } else { + await setTimelinePostPropertyValue(timelineName, id, "type", "image"); + await setTimelinePostPropertyValue( + timelineName, + id, + "data", + content.data + ); + await setTimelinePostPropertyValue( + timelineName, + id, + "etag", + await sha1(content.data) + ); + } + + return (await getTimelinePostInfo( + timelineName, + id + )) as HttpTimelinePostInfo; + } + + async deletePost( + timelineName: string, + postId: number, + _token: string + ): Promise<void> { + await mockPrepare("timeline.post.delete"); + // TODO: permission check + await removeTimelinePostProperty(timelineName, postId, "type"); + await removeTimelinePostProperty(timelineName, postId, "data"); + await removeTimelinePostProperty(timelineName, postId, "etag"); + await setTimelinePostPropertyValue( + timelineName, + postId, + "lastUpdated", + new Date().toISOString() + ); + } +} diff --git a/Timeline/ClientApp/src/app/http/mock/token.ts b/Timeline/ClientApp/src/app/http/mock/token.ts index 6929be2a..0a350894 100644 --- a/Timeline/ClientApp/src/app/http/mock/token.ts +++ b/Timeline/ClientApp/src/app/http/mock/token.ts @@ -1,53 +1,53 @@ -import { AxiosError } from 'axios';
-
-import {
- IHttpTokenClient,
- HttpCreateTokenRequest,
- HttpCreateTokenResponse,
- HttpVerifyTokenRequest,
- HttpVerifyTokenResponse,
-} from '../token';
-
-import { mockPrepare } from './common';
-import { getUser, MockUserNotExistError, checkToken } from './user';
-
-export class MockHttpTokenClient implements IHttpTokenClient {
- // TODO: Mock bad credentials error.
- async create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> {
- await mockPrepare('token.create');
- try {
- const user = await getUser(req.username);
- return {
- user,
- token: `token-${req.username}`,
- };
- } catch (e) {
- if (e instanceof MockUserNotExistError) {
- throw {
- isAxiosError: true,
- response: {
- status: 400,
- },
- } as Partial<AxiosError>;
- }
- throw e;
- }
- }
-
- async verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> {
- await mockPrepare('token.verify');
- try {
- const user = await getUser(checkToken(req.token));
- return {
- user,
- };
- } catch (e) {
- throw {
- isAxiosError: true,
- response: {
- status: 400,
- },
- } as Partial<AxiosError>;
- }
- }
-}
+import { AxiosError } from "axios"; + +import { + IHttpTokenClient, + HttpCreateTokenRequest, + HttpCreateTokenResponse, + HttpVerifyTokenRequest, + HttpVerifyTokenResponse, +} from "../token"; + +import { mockPrepare } from "./common"; +import { getUser, MockUserNotExistError, checkToken } from "./user"; + +export class MockHttpTokenClient implements IHttpTokenClient { + // TODO: Mock bad credentials error. + async create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> { + await mockPrepare("token.create"); + try { + const user = await getUser(req.username); + return { + user, + token: `token-${req.username}`, + }; + } catch (e) { + if (e instanceof MockUserNotExistError) { + throw { + isAxiosError: true, + response: { + status: 400, + }, + } as Partial<AxiosError>; + } + throw e; + } + } + + async verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> { + await mockPrepare("token.verify"); + try { + const user = await getUser(checkToken(req.token)); + return { + user, + }; + } catch (e) { + throw { + isAxiosError: true, + response: { + status: 400, + }, + } as Partial<AxiosError>; + } + } +} diff --git a/Timeline/ClientApp/src/app/http/mock/user.ts b/Timeline/ClientApp/src/app/http/mock/user.ts index 76a35f17..1ecd7365 100644 --- a/Timeline/ClientApp/src/app/http/mock/user.ts +++ b/Timeline/ClientApp/src/app/http/mock/user.ts @@ -1,140 +1,140 @@ -import axios from 'axios';
-
-import { BlobWithEtag, NotModified } from '../common';
-import {
- IHttpUserClient,
- HttpUser,
- HttpUserNotExistError,
- HttpUserPatchRequest,
- HttpChangePasswordRequest,
-} from '../user';
-
-import { mockStorage, sha1, mockPrepare } from './common';
-
-import defaultAvatarUrl from './default-avatar.png';
-
-let _defaultAvatar: BlobWithEtag | undefined = undefined;
-
-async function getDefaultAvatar(): Promise<BlobWithEtag> {
- if (_defaultAvatar == null) {
- const blob = (
- await axios.get<Blob>(defaultAvatarUrl, {
- responseType: 'blob',
- })
- ).data;
- const etag = await sha1(blob);
- _defaultAvatar = {
- data: blob,
- etag,
- };
- }
- return _defaultAvatar;
-}
-
-export class MockTokenError extends Error {
- constructor() {
- super('Token bad format.');
- }
-}
-
-export class MockUserNotExistError extends Error {
- constructor() {
- super('Only two user "user" and "admin".');
- }
-}
-
-export function checkUsername(
- username: string
-): asserts username is 'user' | 'admin' {
- if (!['user', 'admin'].includes(username)) throw new MockUserNotExistError();
-}
-
-export function checkToken(token: string): string {
- if (!token.startsWith('token-')) {
- throw new MockTokenError();
- }
- return token.substr(6);
-}
-
-const uniqueIdMap = {
- user: 'e4c80127d092d9b2fc19c5e04612d4c0',
- admin: '5640fa45435f9a55077b9f77c42a77bb',
-};
-
-export async function getUser(
- username: 'user' | 'admin' | string
-): Promise<HttpUser> {
- checkUsername(username);
- const savedNickname = await mockStorage.getItem<string>(
- `user.${username}.nickname`
- );
- return {
- uniqueId: uniqueIdMap[username],
- username: username,
- nickname:
- savedNickname == null || savedNickname === '' ? username : savedNickname,
- administrator: username === 'admin',
- };
-}
-
-export class MockHttpUserClient implements IHttpUserClient {
- async get(username: string): Promise<HttpUser> {
- await mockPrepare('user.get');
- return await getUser(username).catch((e) => {
- if (e instanceof MockUserNotExistError) {
- throw new HttpUserNotExistError();
- } else {
- throw e;
- }
- });
- }
-
- async patch(
- username: string,
- req: HttpUserPatchRequest,
- _token: string
- ): Promise<HttpUser> {
- await mockPrepare('user.patch');
- if (req.nickname != null) {
- await mockStorage.setItem(`user.${username}.nickname`, req.nickname);
- }
- return await getUser(username);
- }
-
- getAvatar(username: string): Promise<BlobWithEtag>;
- async getAvatar(
- username: string,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- await mockPrepare('user.avatar.get');
-
- const savedEtag = await mockStorage.getItem(`user.${username}.avatar.etag`);
- if (savedEtag == null) {
- return await getDefaultAvatar();
- }
-
- if (savedEtag === etag) {
- return new NotModified();
- }
-
- return {
- data: await mockStorage.getItem<Blob>(`user.${username}.avatar.data`),
- etag: await mockStorage.getItem<string>(`user.${username}.avatar.etag`),
- };
- }
-
- async putAvatar(username: string, data: Blob, _token: string): Promise<void> {
- await mockPrepare('user.avatar.put');
- const etag = await sha1(data);
- await mockStorage.setItem<Blob>(`user.${username}.avatar.data`, data);
- await mockStorage.setItem<string>(`user.${username}.avatar.etag`, etag);
- }
-
- async changePassword(
- _req: HttpChangePasswordRequest,
- _token: string
- ): Promise<void> {
- await mockPrepare('userop.changepassowrd');
- throw new Error('Not Implemented.');
- }
-}
+import axios from "axios"; + +import { BlobWithEtag, NotModified } from "../common"; +import { + IHttpUserClient, + HttpUser, + HttpUserNotExistError, + HttpUserPatchRequest, + HttpChangePasswordRequest, +} from "../user"; + +import { mockStorage, sha1, mockPrepare } from "./common"; + +import defaultAvatarUrl from "./default-avatar.png"; + +let _defaultAvatar: BlobWithEtag | undefined = undefined; + +async function getDefaultAvatar(): Promise<BlobWithEtag> { + if (_defaultAvatar == null) { + const blob = ( + await axios.get<Blob>(defaultAvatarUrl, { + responseType: "blob", + }) + ).data; + const etag = await sha1(blob); + _defaultAvatar = { + data: blob, + etag, + }; + } + return _defaultAvatar; +} + +export class MockTokenError extends Error { + constructor() { + super("Token bad format."); + } +} + +export class MockUserNotExistError extends Error { + constructor() { + super('Only two user "user" and "admin".'); + } +} + +export function checkUsername( + username: string +): asserts username is "user" | "admin" { + if (!["user", "admin"].includes(username)) throw new MockUserNotExistError(); +} + +export function checkToken(token: string): string { + if (!token.startsWith("token-")) { + throw new MockTokenError(); + } + return token.substr(6); +} + +const uniqueIdMap = { + user: "e4c80127d092d9b2fc19c5e04612d4c0", + admin: "5640fa45435f9a55077b9f77c42a77bb", +}; + +export async function getUser( + username: "user" | "admin" | string +): Promise<HttpUser> { + checkUsername(username); + const savedNickname = await mockStorage.getItem<string>( + `user.${username}.nickname` + ); + return { + uniqueId: uniqueIdMap[username], + username: username, + nickname: + savedNickname == null || savedNickname === "" ? username : savedNickname, + administrator: username === "admin", + }; +} + +export class MockHttpUserClient implements IHttpUserClient { + async get(username: string): Promise<HttpUser> { + await mockPrepare("user.get"); + return await getUser(username).catch((e) => { + if (e instanceof MockUserNotExistError) { + throw new HttpUserNotExistError(); + } else { + throw e; + } + }); + } + + async patch( + username: string, + req: HttpUserPatchRequest, + _token: string + ): Promise<HttpUser> { + await mockPrepare("user.patch"); + if (req.nickname != null) { + await mockStorage.setItem(`user.${username}.nickname`, req.nickname); + } + return await getUser(username); + } + + getAvatar(username: string): Promise<BlobWithEtag>; + async getAvatar( + username: string, + etag?: string + ): Promise<BlobWithEtag | NotModified> { + await mockPrepare("user.avatar.get"); + + const savedEtag = await mockStorage.getItem(`user.${username}.avatar.etag`); + if (savedEtag == null) { + return await getDefaultAvatar(); + } + + if (savedEtag === etag) { + return new NotModified(); + } + + return { + data: await mockStorage.getItem<Blob>(`user.${username}.avatar.data`), + etag: await mockStorage.getItem<string>(`user.${username}.avatar.etag`), + }; + } + + async putAvatar(username: string, data: Blob, _token: string): Promise<void> { + await mockPrepare("user.avatar.put"); + const etag = await sha1(data); + await mockStorage.setItem<Blob>(`user.${username}.avatar.data`, data); + await mockStorage.setItem<string>(`user.${username}.avatar.etag`, etag); + } + + async changePassword( + _req: HttpChangePasswordRequest, + _token: string + ): Promise<void> { + await mockPrepare("userop.changepassowrd"); + throw new Error("Not Implemented."); + } +} diff --git a/Timeline/ClientApp/src/app/http/timeline.ts b/Timeline/ClientApp/src/app/http/timeline.ts index c4ebdee9..4a325bca 100644 --- a/Timeline/ClientApp/src/app/http/timeline.ts +++ b/Timeline/ClientApp/src/app/http/timeline.ts @@ -1,543 +1,543 @@ -import axios, { AxiosError } from 'axios';
-
-import { updateQueryString, applyQueryParameters } from '../utilities/url';
-import {
- apiBaseUrl,
- extractResponseData,
- convertToNetworkError,
- base64,
- convertToIfStatusCodeIs,
- convertToIfErrorCodeIs,
- BlobWithEtag,
- NotModified,
- convertToNotModified,
- convertToForbiddenError,
- convertToBlobWithEtag,
-} from './common';
-import { HttpUser } from './user';
-
-export const kTimelineVisibilities = ['Public', 'Register', 'Private'] as const;
-
-export type TimelineVisibility = typeof kTimelineVisibilities[number];
-
-export interface HttpTimelineInfo {
- uniqueId: string;
- name: string;
- description: string;
- owner: HttpUser;
- visibility: TimelineVisibility;
- lastModified: Date;
- members: HttpUser[];
-}
-
-export interface HttpTimelineListQuery {
- visibility?: TimelineVisibility;
- relate?: string;
- relateType?: 'own' | 'join';
-}
-
-export interface HttpTimelinePostRequest {
- name: string;
-}
-
-export interface HttpTimelinePostTextContent {
- type: 'text';
- text: string;
-}
-
-export interface HttpTimelinePostImageContent {
- type: 'image';
-}
-
-export type HttpTimelinePostContent =
- | HttpTimelinePostTextContent
- | HttpTimelinePostImageContent;
-
-export interface HttpTimelinePostInfo {
- id: number;
- content: HttpTimelinePostContent;
- time: Date;
- lastUpdated: Date;
- author: HttpUser;
- deleted: false;
-}
-
-export interface HttpTimelineDeletedPostInfo {
- id: number;
- time: Date;
- lastUpdated: Date;
- author?: HttpUser;
- deleted: true;
-}
-
-export type HttpTimelineGenericPostInfo =
- | HttpTimelinePostInfo
- | HttpTimelineDeletedPostInfo;
-
-export interface HttpTimelinePostPostRequestTextContent {
- type: 'text';
- text: string;
-}
-
-export interface HttpTimelinePostPostRequestImageContent {
- type: 'image';
- data: Blob;
-}
-
-export type HttpTimelinePostPostRequestContent =
- | HttpTimelinePostPostRequestTextContent
- | HttpTimelinePostPostRequestImageContent;
-
-export interface HttpTimelinePostPostRequest {
- content: HttpTimelinePostPostRequestContent;
- time?: Date;
-}
-
-export interface HttpTimelinePatchRequest {
- visibility?: TimelineVisibility;
- description?: string;
-}
-
-export class HttpTimelineNotExistError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class HttpTimelinePostNotExistError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class HttpTimelineNameConflictError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-//-------------------- begin: internal model --------------------
-
-interface RawTimelineInfo {
- uniqueId: string;
- name: string;
- description: string;
- owner: HttpUser;
- visibility: TimelineVisibility;
- lastModified: string;
- members: HttpUser[];
-}
-
-interface RawTimelinePostTextContent {
- type: 'text';
- text: string;
-}
-
-interface RawTimelinePostImageContent {
- type: 'image';
- url: string;
-}
-
-type RawTimelinePostContent =
- | RawTimelinePostTextContent
- | RawTimelinePostImageContent;
-
-interface RawTimelinePostInfo {
- id: number;
- content: HttpTimelinePostContent;
- time: string;
- lastUpdated: string;
- author: HttpUser;
- deleted: false;
-}
-
-interface RawTimelineDeletedPostInfo {
- id: number;
- time: string;
- lastUpdated: string;
- author: HttpUser;
- deleted: true;
-}
-
-type RawTimelineGenericPostInfo =
- | RawTimelinePostInfo
- | RawTimelineDeletedPostInfo;
-
-interface RawTimelinePostPostRequestTextContent {
- type: 'text';
- text: string;
-}
-
-interface RawTimelinePostPostRequestImageContent {
- type: 'image';
- data: string;
-}
-
-type RawTimelinePostPostRequestContent =
- | RawTimelinePostPostRequestTextContent
- | RawTimelinePostPostRequestImageContent;
-
-interface RawTimelinePostPostRequest {
- content: RawTimelinePostPostRequestContent;
- time?: string;
-}
-
-//-------------------- end: internal model --------------------
-
-function processRawTimelineInfo(raw: RawTimelineInfo): HttpTimelineInfo {
- return {
- ...raw,
- lastModified: new Date(raw.lastModified),
- };
-}
-
-function processRawTimelinePostInfo(
- raw: RawTimelinePostInfo
-): HttpTimelinePostInfo;
-function processRawTimelinePostInfo(
- raw: RawTimelineGenericPostInfo
-): HttpTimelineGenericPostInfo;
-function processRawTimelinePostInfo(
- raw: RawTimelineGenericPostInfo
-): HttpTimelineGenericPostInfo {
- return {
- ...raw,
- time: new Date(raw.time),
- lastUpdated: new Date(raw.lastUpdated),
- };
-}
-
-export interface IHttpTimelineClient {
- listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]>;
- getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- }
- ): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- ifModifiedSince: Date;
- }
- ): Promise<HttpTimelineInfo | NotModified>;
- postTimeline(
- req: HttpTimelinePostRequest,
- token: string
- ): Promise<HttpTimelineInfo>;
- patchTimeline(
- timelineName: string,
- req: HttpTimelinePatchRequest,
- token: string
- ): Promise<HttpTimelineInfo>;
- deleteTimeline(timelineName: string, token: string): Promise<void>;
- memberPut(
- timelineName: string,
- username: string,
- token: string
- ): Promise<void>;
- memberDelete(
- timelineName: string,
- username: string,
- token: string
- ): Promise<void>;
- listPost(
- timelineName: string,
- token?: string
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted?: false;
- }
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted: true;
- }
- ): Promise<HttpTimelineGenericPostInfo[]>;
- getPostData(
- timelineName: string,
- postId: number,
- token?: string
- ): Promise<BlobWithEtag>;
- getPostData(
- timelineName: string,
- postId: number,
- token: string | undefined,
- etag: string
- ): Promise<BlobWithEtag | NotModified>;
- postPost(
- timelineName: string,
- req: HttpTimelinePostPostRequest,
- token: string
- ): Promise<HttpTimelinePostInfo>;
- deletePost(
- timelineName: string,
- postId: number,
- token: string
- ): Promise<void>;
-}
-
-export class HttpTimelineClient implements IHttpTimelineClient {
- listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]> {
- return axios
- .get<RawTimelineInfo[]>(
- applyQueryParameters(`${apiBaseUrl}/timelines`, query)
- )
- .then(extractResponseData)
- .then((list) => list.map(processRawTimelineInfo))
- .catch(convertToNetworkError);
- }
-
- getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- }
- ): Promise<HttpTimelineInfo>;
- getTimeline(
- timelineName: string,
- query: {
- checkUniqueId?: string;
- ifModifiedSince: Date;
- }
- ): Promise<HttpTimelineInfo | NotModified>;
- getTimeline(
- timelineName: string,
- query?: {
- checkUniqueId?: string;
- ifModifiedSince?: Date;
- }
- ): Promise<HttpTimelineInfo | NotModified> {
- return axios
- .get<RawTimelineInfo>(
- applyQueryParameters(`${apiBaseUrl}/timelines/${timelineName}`, query)
- )
- .then((res) => {
- if (res.status === 304) {
- return new NotModified();
- } else {
- return processRawTimelineInfo(res.data);
- }
- })
- .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
- .catch(convertToNetworkError);
- }
-
- postTimeline(
- req: HttpTimelinePostRequest,
- token: string
- ): Promise<HttpTimelineInfo> {
- return axios
- .post<RawTimelineInfo>(`${apiBaseUrl}/timelines?token=${token}`, req)
- .then(extractResponseData)
- .then(processRawTimelineInfo)
- .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError))
- .catch(convertToNetworkError);
- }
-
- patchTimeline(
- timelineName: string,
- req: HttpTimelinePatchRequest,
- token: string
- ): Promise<HttpTimelineInfo> {
- return axios
- .patch<RawTimelineInfo>(
- `${apiBaseUrl}/timelines/${timelineName}?token=${token}`,
- req
- )
- .then(extractResponseData)
- .then(processRawTimelineInfo)
- .catch(convertToNetworkError);
- }
-
- deleteTimeline(timelineName: string, token: string): Promise<void> {
- return axios
- .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`)
- .catch(convertToNetworkError)
- .then();
- }
-
- memberPut(
- timelineName: string,
- username: string,
- token: string
- ): Promise<void> {
- return axios
- .put(
- `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}`
- )
- .catch(convertToNetworkError)
- .then();
- }
-
- memberDelete(
- timelineName: string,
- username: string,
- token: string
- ): Promise<void> {
- return axios
- .delete(
- `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}`
- )
- .catch(convertToNetworkError)
- .then();
- }
-
- listPost(
- timelineName: string,
- token?: string
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted?: false;
- }
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- token: string | undefined,
- query: {
- modifiedSince?: Date;
- includeDeleted: true;
- }
- ): Promise<HttpTimelineGenericPostInfo[]>;
- listPost(
- timelineName: string,
- token?: string,
- query?: {
- modifiedSince?: Date;
- includeDeleted?: boolean;
- }
- ): Promise<HttpTimelineGenericPostInfo[]> {
- let url = `${apiBaseUrl}/timelines/${timelineName}/posts`;
- url = updateQueryString('token', token, url);
- if (query != null) {
- if (query.modifiedSince != null) {
- url = updateQueryString(
- 'modifiedSince',
- query.modifiedSince.toISOString(),
- url
- );
- }
- if (query.includeDeleted != null) {
- url = updateQueryString(
- 'includeDeleted',
- query.includeDeleted ? 'true' : 'false',
- url
- );
- }
- }
-
- return axios
- .get<RawTimelineGenericPostInfo[]>(url)
- .then(extractResponseData)
- .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
- .catch(convertToForbiddenError)
- .catch(convertToNetworkError)
- .then((rawPosts) =>
- rawPosts.map((raw) => processRawTimelinePostInfo(raw))
- );
- }
-
- getPostData(
- timelineName: string,
- postId: number,
- token: string
- ): Promise<BlobWithEtag>;
- getPostData(
- timelineName: string,
- postId: number,
- token?: string,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- const headers =
- etag != null
- ? {
- 'If-None-Match': etag,
- }
- : undefined;
-
- let url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`;
- url = updateQueryString('token', token, url);
-
- return axios
- .get(url, {
- responseType: 'blob',
- headers,
- })
- .then(convertToBlobWithEtag)
- .catch(convertToNotModified)
- .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError))
- .catch(convertToNetworkError);
- }
-
- async postPost(
- timelineName: string,
- req: HttpTimelinePostPostRequest,
- token: string
- ): Promise<HttpTimelinePostInfo> {
- let content: RawTimelinePostPostRequestContent;
- if (req.content.type === 'image') {
- const base64Data = await base64(req.content.data);
- content = {
- ...req.content,
- data: base64Data,
- } as RawTimelinePostPostRequestImageContent;
- } else {
- content = req.content;
- }
- const rawReq: RawTimelinePostPostRequest = {
- content,
- };
- if (req.time != null) {
- rawReq.time = req.time.toISOString();
- }
- return await axios
- .post<RawTimelinePostInfo>(
- `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`,
- rawReq
- )
- .then(extractResponseData)
- .catch(convertToNetworkError)
- .then((rawPost) => processRawTimelinePostInfo(rawPost));
- }
-
- deletePost(
- timelineName: string,
- postId: number,
- token: string
- ): Promise<void> {
- return axios
- .delete(
- `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}?token=${token}`
- )
- .catch(convertToNetworkError)
- .then();
- }
-}
-
-let client: IHttpTimelineClient = new HttpTimelineClient();
-
-export function getHttpTimelineClient(): IHttpTimelineClient {
- return client;
-}
-
-export function setHttpTimelineClient(
- newClient: IHttpTimelineClient
-): IHttpTimelineClient {
- const old = client;
- client = newClient;
- return old;
-}
+import axios, { AxiosError } from "axios"; + +import { updateQueryString, applyQueryParameters } from "../utilities/url"; +import { + apiBaseUrl, + extractResponseData, + convertToNetworkError, + base64, + convertToIfStatusCodeIs, + convertToIfErrorCodeIs, + BlobWithEtag, + NotModified, + convertToNotModified, + convertToForbiddenError, + convertToBlobWithEtag, +} from "./common"; +import { HttpUser } from "./user"; + +export const kTimelineVisibilities = ["Public", "Register", "Private"] as const; + +export type TimelineVisibility = typeof kTimelineVisibilities[number]; + +export interface HttpTimelineInfo { + uniqueId: string; + name: string; + description: string; + owner: HttpUser; + visibility: TimelineVisibility; + lastModified: Date; + members: HttpUser[]; +} + +export interface HttpTimelineListQuery { + visibility?: TimelineVisibility; + relate?: string; + relateType?: "own" | "join"; +} + +export interface HttpTimelinePostRequest { + name: string; +} + +export interface HttpTimelinePostTextContent { + type: "text"; + text: string; +} + +export interface HttpTimelinePostImageContent { + type: "image"; +} + +export type HttpTimelinePostContent = + | HttpTimelinePostTextContent + | HttpTimelinePostImageContent; + +export interface HttpTimelinePostInfo { + id: number; + content: HttpTimelinePostContent; + time: Date; + lastUpdated: Date; + author: HttpUser; + deleted: false; +} + +export interface HttpTimelineDeletedPostInfo { + id: number; + time: Date; + lastUpdated: Date; + author?: HttpUser; + deleted: true; +} + +export type HttpTimelineGenericPostInfo = + | HttpTimelinePostInfo + | HttpTimelineDeletedPostInfo; + +export interface HttpTimelinePostPostRequestTextContent { + type: "text"; + text: string; +} + +export interface HttpTimelinePostPostRequestImageContent { + type: "image"; + data: Blob; +} + +export type HttpTimelinePostPostRequestContent = + | HttpTimelinePostPostRequestTextContent + | HttpTimelinePostPostRequestImageContent; + +export interface HttpTimelinePostPostRequest { + content: HttpTimelinePostPostRequestContent; + time?: Date; +} + +export interface HttpTimelinePatchRequest { + visibility?: TimelineVisibility; + description?: string; +} + +export class HttpTimelineNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpTimelinePostNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpTimelineNameConflictError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +//-------------------- begin: internal model -------------------- + +interface RawTimelineInfo { + uniqueId: string; + name: string; + description: string; + owner: HttpUser; + visibility: TimelineVisibility; + lastModified: string; + members: HttpUser[]; +} + +interface RawTimelinePostTextContent { + type: "text"; + text: string; +} + +interface RawTimelinePostImageContent { + type: "image"; + url: string; +} + +type RawTimelinePostContent = + | RawTimelinePostTextContent + | RawTimelinePostImageContent; + +interface RawTimelinePostInfo { + id: number; + content: HttpTimelinePostContent; + time: string; + lastUpdated: string; + author: HttpUser; + deleted: false; +} + +interface RawTimelineDeletedPostInfo { + id: number; + time: string; + lastUpdated: string; + author: HttpUser; + deleted: true; +} + +type RawTimelineGenericPostInfo = + | RawTimelinePostInfo + | RawTimelineDeletedPostInfo; + +interface RawTimelinePostPostRequestTextContent { + type: "text"; + text: string; +} + +interface RawTimelinePostPostRequestImageContent { + type: "image"; + data: string; +} + +type RawTimelinePostPostRequestContent = + | RawTimelinePostPostRequestTextContent + | RawTimelinePostPostRequestImageContent; + +interface RawTimelinePostPostRequest { + content: RawTimelinePostPostRequestContent; + time?: string; +} + +//-------------------- end: internal model -------------------- + +function processRawTimelineInfo(raw: RawTimelineInfo): HttpTimelineInfo { + return { + ...raw, + lastModified: new Date(raw.lastModified), + }; +} + +function processRawTimelinePostInfo( + raw: RawTimelinePostInfo +): HttpTimelinePostInfo; +function processRawTimelinePostInfo( + raw: RawTimelineGenericPostInfo +): HttpTimelineGenericPostInfo; +function processRawTimelinePostInfo( + raw: RawTimelineGenericPostInfo +): HttpTimelineGenericPostInfo { + return { + ...raw, + time: new Date(raw.time), + lastUpdated: new Date(raw.lastUpdated), + }; +} + +export interface IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]>; + getTimeline(timelineName: string): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise<HttpTimelineInfo | NotModified>; + postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise<HttpTimelineInfo>; + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + token: string + ): Promise<HttpTimelineInfo>; + deleteTimeline(timelineName: string, token: string): Promise<void>; + memberPut( + timelineName: string, + username: string, + token: string + ): Promise<void>; + memberDelete( + timelineName: string, + username: string, + token: string + ): Promise<void>; + listPost( + timelineName: string, + token?: string + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise<HttpTimelineGenericPostInfo[]>; + getPostData( + timelineName: string, + postId: number, + token?: string + ): Promise<BlobWithEtag>; + getPostData( + timelineName: string, + postId: number, + token: string | undefined, + etag: string + ): Promise<BlobWithEtag | NotModified>; + postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise<HttpTimelinePostInfo>; + deletePost( + timelineName: string, + postId: number, + token: string + ): Promise<void>; +} + +export class HttpTimelineClient implements IHttpTimelineClient { + listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]> { + return axios + .get<RawTimelineInfo[]>( + applyQueryParameters(`${apiBaseUrl}/timelines`, query) + ) + .then(extractResponseData) + .then((list) => list.map(processRawTimelineInfo)) + .catch(convertToNetworkError); + } + + getTimeline(timelineName: string): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + } + ): Promise<HttpTimelineInfo>; + getTimeline( + timelineName: string, + query: { + checkUniqueId?: string; + ifModifiedSince: Date; + } + ): Promise<HttpTimelineInfo | NotModified>; + getTimeline( + timelineName: string, + query?: { + checkUniqueId?: string; + ifModifiedSince?: Date; + } + ): Promise<HttpTimelineInfo | NotModified> { + return axios + .get<RawTimelineInfo>( + applyQueryParameters(`${apiBaseUrl}/timelines/${timelineName}`, query) + ) + .then((res) => { + if (res.status === 304) { + return new NotModified(); + } else { + return processRawTimelineInfo(res.data); + } + }) + .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError)) + .catch(convertToNetworkError); + } + + postTimeline( + req: HttpTimelinePostRequest, + token: string + ): Promise<HttpTimelineInfo> { + return axios + .post<RawTimelineInfo>(`${apiBaseUrl}/timelines?token=${token}`, req) + .then(extractResponseData) + .then(processRawTimelineInfo) + .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError)) + .catch(convertToNetworkError); + } + + patchTimeline( + timelineName: string, + req: HttpTimelinePatchRequest, + token: string + ): Promise<HttpTimelineInfo> { + return axios + .patch<RawTimelineInfo>( + `${apiBaseUrl}/timelines/${timelineName}?token=${token}`, + req + ) + .then(extractResponseData) + .then(processRawTimelineInfo) + .catch(convertToNetworkError); + } + + deleteTimeline(timelineName: string, token: string): Promise<void> { + return axios + .delete(`${apiBaseUrl}/timelines/${timelineName}?token=${token}`) + .catch(convertToNetworkError) + .then(); + } + + memberPut( + timelineName: string, + username: string, + token: string + ): Promise<void> { + return axios + .put( + `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } + + memberDelete( + timelineName: string, + username: string, + token: string + ): Promise<void> { + return axios + .delete( + `${apiBaseUrl}/timelines/${timelineName}/members/${username}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } + + listPost( + timelineName: string, + token?: string + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted?: false; + } + ): Promise<HttpTimelinePostInfo[]>; + listPost( + timelineName: string, + token: string | undefined, + query: { + modifiedSince?: Date; + includeDeleted: true; + } + ): Promise<HttpTimelineGenericPostInfo[]>; + listPost( + timelineName: string, + token?: string, + query?: { + modifiedSince?: Date; + includeDeleted?: boolean; + } + ): Promise<HttpTimelineGenericPostInfo[]> { + let url = `${apiBaseUrl}/timelines/${timelineName}/posts`; + url = updateQueryString("token", token, url); + if (query != null) { + if (query.modifiedSince != null) { + url = updateQueryString( + "modifiedSince", + query.modifiedSince.toISOString(), + url + ); + } + if (query.includeDeleted != null) { + url = updateQueryString( + "includeDeleted", + query.includeDeleted ? "true" : "false", + url + ); + } + } + + return axios + .get<RawTimelineGenericPostInfo[]>(url) + .then(extractResponseData) + .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError)) + .catch(convertToForbiddenError) + .catch(convertToNetworkError) + .then((rawPosts) => + rawPosts.map((raw) => processRawTimelinePostInfo(raw)) + ); + } + + getPostData( + timelineName: string, + postId: number, + token: string + ): Promise<BlobWithEtag>; + getPostData( + timelineName: string, + postId: number, + token?: string, + etag?: string + ): Promise<BlobWithEtag | NotModified> { + const headers = + etag != null + ? { + "If-None-Match": etag, + } + : undefined; + + let url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`; + url = updateQueryString("token", token, url); + + return axios + .get(url, { + responseType: "blob", + headers, + }) + .then(convertToBlobWithEtag) + .catch(convertToNotModified) + .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError)) + .catch(convertToNetworkError); + } + + async postPost( + timelineName: string, + req: HttpTimelinePostPostRequest, + token: string + ): Promise<HttpTimelinePostInfo> { + let content: RawTimelinePostPostRequestContent; + if (req.content.type === "image") { + const base64Data = await base64(req.content.data); + content = { + ...req.content, + data: base64Data, + } as RawTimelinePostPostRequestImageContent; + } else { + content = req.content; + } + const rawReq: RawTimelinePostPostRequest = { + content, + }; + if (req.time != null) { + rawReq.time = req.time.toISOString(); + } + return await axios + .post<RawTimelinePostInfo>( + `${apiBaseUrl}/timelines/${timelineName}/posts?token=${token}`, + rawReq + ) + .then(extractResponseData) + .catch(convertToNetworkError) + .then((rawPost) => processRawTimelinePostInfo(rawPost)); + } + + deletePost( + timelineName: string, + postId: number, + token: string + ): Promise<void> { + return axios + .delete( + `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}?token=${token}` + ) + .catch(convertToNetworkError) + .then(); + } +} + +let client: IHttpTimelineClient = new HttpTimelineClient(); + +export function getHttpTimelineClient(): IHttpTimelineClient { + return client; +} + +export function setHttpTimelineClient( + newClient: IHttpTimelineClient +): IHttpTimelineClient { + const old = client; + client = newClient; + return old; +} diff --git a/Timeline/ClientApp/src/app/http/token.ts b/Timeline/ClientApp/src/app/http/token.ts index 26f7891f..ae0cf3f6 100644 --- a/Timeline/ClientApp/src/app/http/token.ts +++ b/Timeline/ClientApp/src/app/http/token.ts @@ -1,72 +1,72 @@ -import axios, { AxiosError } from 'axios';
-
-import {
- apiBaseUrl,
- convertToNetworkError,
- convertToIfErrorCodeIs,
- extractResponseData,
-} from './common';
-import { HttpUser } from './user';
-
-export interface HttpCreateTokenRequest {
- username: string;
- password: string;
- expire: number;
-}
-
-export interface HttpCreateTokenResponse {
- token: string;
- user: HttpUser;
-}
-
-export interface HttpVerifyTokenRequest {
- token: string;
-}
-
-export interface HttpVerifyTokenResponse {
- user: HttpUser;
-}
-
-export class HttpCreateTokenBadCredentialError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export interface IHttpTokenClient {
- create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse>;
- verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse>;
-}
-
-export class HttpTokenClient implements IHttpTokenClient {
- create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> {
- return axios
- .post<HttpCreateTokenResponse>(`${apiBaseUrl}/token/create`, req)
- .then(extractResponseData)
- .catch(
- convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError)
- )
- .catch(convertToNetworkError);
- }
-
- verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> {
- return axios
- .post<HttpVerifyTokenResponse>(`${apiBaseUrl}/token/verify`, req)
- .then(extractResponseData)
- .catch(convertToNetworkError);
- }
-}
-
-let client: IHttpTokenClient = new HttpTokenClient();
-
-export function getHttpTokenClient(): IHttpTokenClient {
- return client;
-}
-
-export function setHttpTokenClient(
- newClient: IHttpTokenClient
-): IHttpTokenClient {
- const old = client;
- client = newClient;
- return old;
-}
+import axios, { AxiosError } from "axios"; + +import { + apiBaseUrl, + convertToNetworkError, + convertToIfErrorCodeIs, + extractResponseData, +} from "./common"; +import { HttpUser } from "./user"; + +export interface HttpCreateTokenRequest { + username: string; + password: string; + expire: number; +} + +export interface HttpCreateTokenResponse { + token: string; + user: HttpUser; +} + +export interface HttpVerifyTokenRequest { + token: string; +} + +export interface HttpVerifyTokenResponse { + user: HttpUser; +} + +export class HttpCreateTokenBadCredentialError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpTokenClient { + create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse>; + verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse>; +} + +export class HttpTokenClient implements IHttpTokenClient { + create(req: HttpCreateTokenRequest): Promise<HttpCreateTokenResponse> { + return axios + .post<HttpCreateTokenResponse>(`${apiBaseUrl}/token/create`, req) + .then(extractResponseData) + .catch( + convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError) + ) + .catch(convertToNetworkError); + } + + verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> { + return axios + .post<HttpVerifyTokenResponse>(`${apiBaseUrl}/token/verify`, req) + .then(extractResponseData) + .catch(convertToNetworkError); + } +} + +let client: IHttpTokenClient = new HttpTokenClient(); + +export function getHttpTokenClient(): IHttpTokenClient { + return client; +} + +export function setHttpTokenClient( + newClient: IHttpTokenClient +): IHttpTokenClient { + const old = client; + client = newClient; + return old; +} diff --git a/Timeline/ClientApp/src/app/http/user.ts b/Timeline/ClientApp/src/app/http/user.ts index baa831aa..a0a02cce 100644 --- a/Timeline/ClientApp/src/app/http/user.ts +++ b/Timeline/ClientApp/src/app/http/user.ts @@ -1,134 +1,134 @@ -import axios, { AxiosError } from 'axios';
-
-import {
- apiBaseUrl,
- convertToNetworkError,
- extractResponseData,
- convertToIfStatusCodeIs,
- convertToIfErrorCodeIs,
- NotModified,
- BlobWithEtag,
- convertToBlobWithEtag,
- convertToNotModified,
-} from './common';
-
-export interface HttpUser {
- uniqueId: string;
- username: string;
- administrator: boolean;
- nickname: string;
-}
-
-export interface HttpUserPatchRequest {
- nickname?: string;
-}
-
-export interface HttpChangePasswordRequest {
- oldPassword: string;
- newPassword: string;
-}
-
-export class HttpUserNotExistError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class HttpChangePasswordBadCredentialError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export interface IHttpUserClient {
- get(username: string): Promise<HttpUser>;
- patch(
- username: string,
- req: HttpUserPatchRequest,
- token: string
- ): Promise<HttpUser>;
- getAvatar(username: string): Promise<BlobWithEtag>;
- getAvatar(
- username: string,
- etag: string
- ): Promise<BlobWithEtag | NotModified>;
- putAvatar(username: string, data: Blob, token: string): Promise<void>;
- changePassword(req: HttpChangePasswordRequest, token: string): Promise<void>;
-}
-
-export class HttpUserClient implements IHttpUserClient {
- get(username: string): Promise<HttpUser> {
- return axios
- .get<HttpUser>(`${apiBaseUrl}/users/${username}`)
- .then(extractResponseData)
- .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
- .catch(convertToNetworkError);
- }
-
- patch(
- username: string,
- req: HttpUserPatchRequest,
- token: string
- ): Promise<HttpUser> {
- return axios
- .patch<HttpUser>(`${apiBaseUrl}/users/${username}?token=${token}`, req)
- .then(extractResponseData)
- .catch(convertToNetworkError);
- }
-
- getAvatar(username: string): Promise<BlobWithEtag>;
- getAvatar(
- username: string,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- const headers =
- etag != null
- ? {
- 'If-None-Match': etag,
- }
- : undefined;
-
- return axios
- .get(`${apiBaseUrl}/users/${username}/avatar`, {
- responseType: 'blob',
- headers,
- })
- .then(convertToBlobWithEtag)
- .catch(convertToNotModified)
- .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
- .catch(convertToNetworkError);
- }
-
- putAvatar(username: string, data: Blob, token: string): Promise<void> {
- return axios
- .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, {
- headers: {
- 'Content-Type': data.type,
- },
- })
- .catch(convertToNetworkError)
- .then();
- }
-
- changePassword(req: HttpChangePasswordRequest, token: string): Promise<void> {
- return axios
- .post(`${apiBaseUrl}/userop/changepassword?token=${token}`, req)
- .catch(
- convertToIfErrorCodeIs(11020201, HttpChangePasswordBadCredentialError)
- )
- .catch(convertToNetworkError)
- .then();
- }
-}
-
-let client: IHttpUserClient = new HttpUserClient();
-
-export function getHttpUserClient(): IHttpUserClient {
- return client;
-}
-
-export function setHttpUserClient(newClient: IHttpUserClient): IHttpUserClient {
- const old = client;
- client = newClient;
- return old;
-}
+import axios, { AxiosError } from "axios"; + +import { + apiBaseUrl, + convertToNetworkError, + extractResponseData, + convertToIfStatusCodeIs, + convertToIfErrorCodeIs, + NotModified, + BlobWithEtag, + convertToBlobWithEtag, + convertToNotModified, +} from "./common"; + +export interface HttpUser { + uniqueId: string; + username: string; + administrator: boolean; + nickname: string; +} + +export interface HttpUserPatchRequest { + nickname?: string; +} + +export interface HttpChangePasswordRequest { + oldPassword: string; + newPassword: string; +} + +export class HttpUserNotExistError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export class HttpChangePasswordBadCredentialError extends Error { + constructor(public innerError?: AxiosError) { + super(); + } +} + +export interface IHttpUserClient { + get(username: string): Promise<HttpUser>; + patch( + username: string, + req: HttpUserPatchRequest, + token: string + ): Promise<HttpUser>; + getAvatar(username: string): Promise<BlobWithEtag>; + getAvatar( + username: string, + etag: string + ): Promise<BlobWithEtag | NotModified>; + putAvatar(username: string, data: Blob, token: string): Promise<void>; + changePassword(req: HttpChangePasswordRequest, token: string): Promise<void>; +} + +export class HttpUserClient implements IHttpUserClient { + get(username: string): Promise<HttpUser> { + return axios + .get<HttpUser>(`${apiBaseUrl}/users/${username}`) + .then(extractResponseData) + .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError)) + .catch(convertToNetworkError); + } + + patch( + username: string, + req: HttpUserPatchRequest, + token: string + ): Promise<HttpUser> { + return axios + .patch<HttpUser>(`${apiBaseUrl}/users/${username}?token=${token}`, req) + .then(extractResponseData) + .catch(convertToNetworkError); + } + + getAvatar(username: string): Promise<BlobWithEtag>; + getAvatar( + username: string, + etag?: string + ): Promise<BlobWithEtag | NotModified> { + const headers = + etag != null + ? { + "If-None-Match": etag, + } + : undefined; + + return axios + .get(`${apiBaseUrl}/users/${username}/avatar`, { + responseType: "blob", + headers, + }) + .then(convertToBlobWithEtag) + .catch(convertToNotModified) + .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError)) + .catch(convertToNetworkError); + } + + putAvatar(username: string, data: Blob, token: string): Promise<void> { + return axios + .put(`${apiBaseUrl}/users/${username}/avatar?token=${token}`, data, { + headers: { + "Content-Type": data.type, + }, + }) + .catch(convertToNetworkError) + .then(); + } + + changePassword(req: HttpChangePasswordRequest, token: string): Promise<void> { + return axios + .post(`${apiBaseUrl}/userop/changepassword?token=${token}`, req) + .catch( + convertToIfErrorCodeIs(11020201, HttpChangePasswordBadCredentialError) + ) + .catch(convertToNetworkError) + .then(); + } +} + +let client: IHttpUserClient = new HttpUserClient(); + +export function getHttpUserClient(): IHttpUserClient { + return client; +} + +export function setHttpUserClient(newClient: IHttpUserClient): IHttpUserClient { + const old = client; + client = newClient; + return old; +} |