Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type definitions for Request and Response #10724

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 343 additions & 3 deletions modules/lib/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ export interface ScriptValue {

hasMember(key: string): boolean;

getMember(key: string): ScriptValueDefinition;
getMember(key: string): ScriptValue;

getArray(): ScriptValueDefinition[];
getArray(): ScriptValue[];

getMap(): Record<string, unknown>;

Expand All @@ -142,6 +142,339 @@ export interface ScriptValue {
export type XpRequire = <Key extends keyof XpLibraries | string = string>(path: Key) =>
Key extends keyof XpLibraries ? XpLibraries[Key] : unknown;

export type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>);

export type StrictMergeInterfaces<
ComLock marked this conversation as resolved.
Show resolved Hide resolved
A,
B = Record<string, never>
> = B extends Record<string, never>
? A
: Pick<A, Exclude<keyof A, keyof B>> & B;

export interface ComplexCookie {
/**
* The value of the cookie (optional).
*
* @type string
*/
value: string
ComLock marked this conversation as resolved.
Show resolved Hide resolved
/**
* A comment (rfc2109) to document the cookie (optional).
*
* @type string
*/
comment?: string
/**
* The expiration date and time for the cookie (optional).
*
* @type string
*/
expires?: Date;
/**
* The domain name for which the cookie is set.
*
* @type string
*/
domain?: string
/**
* Indicates whether the cookie should not be accessible via JavaScript (optional).
*
* @type string
*/
httpOnly?: boolean
/**
* The maximum age of the cookie in seconds (optional).
*
* @type string
*/
maxAge?: number
/**
* The path on the server where the cookie should be available.
*
* @type string
*/
path?: string
/**
* Specifies the SameSite attribute (draft RFC) for the cookie (optional).
*
* @type string
*/
sameSite?: LiteralUnion<'lax' | 'strict' | 'none'>;
/**
* Indicates whether the cookie should only be sent over HTTPS (optional).
*
* @type string
*/
secure?: boolean
}

export type RequestBranch = 'draft' | 'master';
export type RequestGetHeaderFunction = (headerName: string) => string | null;
export type RequestMethod = 'GET' | 'POST' | ' HEAD' | 'OPTIONS' | ' PUT' | 'DELETE';
ComLock marked this conversation as resolved.
Show resolved Hide resolved
export type RequestMode = 'edit' | 'inline' | 'live' | 'preview' | 'admin';
export type RequestParams = Record<string, string | string[]>;
export type RequestScheme = 'http' | 'https';
ComLock marked this conversation as resolved.
Show resolved Hide resolved

export type RequestCookies = Record<string, string | undefined>;
export type ResponseCookies = Record<string, string | ComplexCookie | undefined>;

export type RequestHeaders = Record<string, string | undefined>;
export type ResponseHeaders = Record<string, string | number | (string | number)[] | undefined>;

export interface DefaultRequestCookies extends RequestCookies {
JSESSIONID?: string
}

export interface DefaultRequestHeaders extends RequestHeaders {
Accept?: string
'Accept-Charset'?: string
'Accept-Encoding'?: string
'Accept-Language'?: string
Authorization?: string
'Cache-Control'?: string
Connection?: string
'Content-Length'?: string
'Content-Type'?: string
Cookie?: string
Language?: string
Host?: string
'If-None-Match'?: string
Referer?: string
'sec-ch-ua'?: string
'sec-ch-ua-mobile'?: string
'sec-ch-ua-platform'?: string
'Sec-Fetch-Dest'?: string
'Sec-Fetch-Mode'?: string
'Sec-Fetch-Site'?: string
'Sec-Fetch-User'?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some experimental headers whe really never use. Why are they int default? Why they are different case BTW?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default optional, only for autocomplete. These are the ones in chrome.

Dunno what to use them for on the server-side, so probably can be removed.

Are they in the way though?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that we need experimental headers in the default list. Most of them are not added automatically, and for some, like Sec-CH-UA-Mobile, I don’t even see the point in using them in XP.
My suggestion is as follows: drop them. If a developer really needs them, they can always add them at their own risk.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the experimental ones and added autocompletion to the Sec-Fetch ones.

I also added Priority since both Chrome and Firefox sends it.

There are tons of headers:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields

Not really sure which ones should be part of DefaultRequestHeaders.

'Upgrade-Insecure-Requests'?: string
'User-Agent'?: string
'X-Forwarded-For'?: string
'X-Forwarded-Host'?: string
'X-Forwarded-Proto'?: string
'X-Forwarded-Server'?: string
}

export interface RequestConstructorParams {
body?: string
branch?: LiteralUnion<RequestBranch>
contentType?: string
contextPath?: string
cookies: RequestCookies
headers: RequestHeaders
host: string
method: LiteralUnion<RequestMethod>
mode: LiteralUnion<RequestMode>
params: RequestParams
ComLock marked this conversation as resolved.
Show resolved Hide resolved
path: string
port: number
rawPath: string
remoteAddress: string
repositoryId?: string
scheme: LiteralUnion<RequestScheme>
url: string
validTicket?: boolean
webSocket: boolean
}

export interface RequestInterface extends RequestConstructorParams {
getHeader: RequestGetHeaderFunction
}

export interface DefaultRequest extends RequestInterface {
cookies: DefaultRequestCookies
headers: DefaultRequestHeaders
}

export type Request<
T extends Partial<RequestInterface> = Record<string, never>
> = StrictMergeInterfaces<DefaultRequest, T>;

export type SerializableRequest<T extends RequestInterface = DefaultRequest> = Omit<
Partial<T>,
'body' | 'contextPath' | 'rawPath' | 'repositoryId' | 'webSocket'
> & {
body?: unknown[] | Record<string, unknown> | boolean | number | string | null;
};

export interface DefaultResponseHeaders extends ResponseHeaders {
'Cache-Control'?: string
'Content-Encoding'?: string
'Content-Type'?: string
'Content-Security-Policy'?: string
'Date'?: string
ComLock marked this conversation as resolved.
Show resolved Hide resolved
Etag?: string | number
Location?: string
}

// NOTE Even though PortalResponseSerializer allows non-array values,
// when it comes back from Java the PortalResponseMapper will always return an array.
// So perhaps it's better to enforce that, so it's consistent both ways.
// It also causes problems in ResponseProcessors, since they work with both from and to Java.
export interface PageContributions {
headBegin?: string[]
headEnd?: string[]
bodyBegin?: string[]
bodyEnd?: string[]
}

export type ResponseBody = unknown[] | Record<string, unknown> | boolean | number | string | null | ByteSource;

export interface MappedResponse {
applyFilters: boolean
body?: ResponseBody
contentType: string
cookies: ResponseCookies
headers: ResponseHeaders
pageContributions: PageContributions
postProcess: boolean
status: number
}

export interface ResponseInterface extends Partial<MappedResponse> {
redirect?: string
}

export interface DefaultResponse extends ResponseInterface {
contentType?: LiteralUnion<'text/html' | 'application/json'>
headers?: DefaultResponseHeaders
}

export type Response<
T extends Partial<ResponseInterface> = Record<string, never>
> = StrictMergeInterfaces<DefaultResponse, T>;

export type RequestHandler<
RequestFromJava extends RequestInterface = DefaultRequest,
ResponseToJava extends ResponseInterface = DefaultResponse
> = (request: RequestFromJava) => ResponseToJava;

export type HttpFilterNext<
RequestToJava extends SerializableRequest = SerializableRequest<DefaultRequest>,
ResponseToJava extends ResponseInterface = DefaultResponse
> = (request: RequestToJava) => ResponseToJava;

export interface WebSocketSession {
id: string
path: string

// TODO Maybe Record<string, string | string[]>
// See com.enonic.xp.portal.impl.mapper.WebSocketEventMapper
// And com.enonic.xp.portal.impl.mapper.MapperHelper
params: Record<string, unknown>
ComLock marked this conversation as resolved.
Show resolved Hide resolved

user: Omit<User,'type'>
}

export type WebSocketEventType = 'open' | 'message' | 'error' | 'close';

export interface WebSocketEvent<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my implementation of WebSocketEvent. It uses a discriminated union based on the type to ensure the shape of the object.

type AbstractWebSocketEvent<WebSocketData> = {
  session: {
    id: string;
    path: string;
    params: { [key: string]: string | undefined };
  };
  data: WebSocketData;
};

export type OpenWebSocketEvent<WebSocketData = {}> = AbstractWebSocketEvent<WebSocketData> & {
  type: "open";
};

export type MessageWebSocketEvent<WebSocketData = {}> = AbstractWebSocketEvent<WebSocketData> & {
  type: "message";
  message: string;
};

export type CloseWebSocketEvent<WebSocketData = {}> = AbstractWebSocketEvent<WebSocketData> & {
  type: "close";
  closeReason: number;
};

export type ErrorWebSocketEvent<WebSocketData = {}> = AbstractWebSocketEvent<WebSocketData> & {
  type: "error";
  error: string;
};

export type WebSocketEvent<WebSocketData = {}> =
  | OpenWebSocketEvent<WebSocketData>
  | MessageWebSocketEvent<WebSocketData>
  | CloseWebSocketEvent<WebSocketData>
  | ErrorWebSocketEvent<WebSocketData>;

Just consider if this pattern might be right for you too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first implementation was similar to that, because it was based on the data I was able to generate in real life. I just simplified after looking at the Java class behind it.

data: T
closeReason?: number
error?: string
message?: string
session: WebSocketSession
type: WebSocketEventType
}

type WebSocketEventHandler<T> = (event: WebSocketEvent<T>) => void;

export interface Controller {
all?: RequestHandler
delete?: RequestHandler
get?: RequestHandler
head?: RequestHandler
options?: RequestHandler
post?: RequestHandler
put?: RequestHandler
webSocketEvent?: WebSocketEventHandler
}

export interface ErrorRequest<T extends RequestInterface = DefaultRequest> {
exception?: unknown
message: string
request: T
status: number
}

export type ErrorRequestHandler<
Err extends ErrorRequest = ErrorRequest,
ResponseToJava extends ResponseInterface = DefaultResponse
> = (err: Err) => ResponseToJava;

export interface ErrorController {
handle400?: ErrorRequestHandler
handle401?: ErrorRequestHandler
handle402?: ErrorRequestHandler
handle403?: ErrorRequestHandler
handle404?: ErrorRequestHandler
handle405?: ErrorRequestHandler
handle406?: ErrorRequestHandler
handle407?: ErrorRequestHandler
handle408?: ErrorRequestHandler
handle409?: ErrorRequestHandler
handle410?: ErrorRequestHandler
handle411?: ErrorRequestHandler
handle412?: ErrorRequestHandler
handle413?: ErrorRequestHandler
handle414?: ErrorRequestHandler
handle415?: ErrorRequestHandler
handle416?: ErrorRequestHandler
handle417?: ErrorRequestHandler
handle418?: ErrorRequestHandler
handle421?: ErrorRequestHandler
handle422?: ErrorRequestHandler
handle423?: ErrorRequestHandler
handle424?: ErrorRequestHandler
handle425?: ErrorRequestHandler
handle426?: ErrorRequestHandler
handle428?: ErrorRequestHandler
handle429?: ErrorRequestHandler
handle431?: ErrorRequestHandler
handle451?: ErrorRequestHandler
handle500?: ErrorRequestHandler
handle501?: ErrorRequestHandler
handle502?: ErrorRequestHandler
handle503?: ErrorRequestHandler
handle504?: ErrorRequestHandler
handle505?: ErrorRequestHandler
handle506?: ErrorRequestHandler
handle507?: ErrorRequestHandler
handle508?: ErrorRequestHandler
handle510?: ErrorRequestHandler
handle511?: ErrorRequestHandler
handleError?: ErrorRequestHandler
}
ComLock marked this conversation as resolved.
Show resolved Hide resolved

export interface IdProviderController extends Controller {
autoLogin?: RequestHandler
handle401?: RequestHandler
login?: RequestHandler
logout?: RequestHandler
}

export interface HttpFilterController<
RequestFromJava extends RequestInterface = DefaultRequest,
ResponseFromNext extends ResponseInterface = Response,
ResponseToJava extends ResponseInterface = ResponseFromNext
> {
filter: (
request: RequestFromJava,
next: HttpFilterNext<
SerializableRequest<RequestFromJava>,
ResponseFromNext
>
) => ResponseToJava;
}

export interface ResponseProcessorController<
RequestFromJava extends RequestInterface = DefaultRequest,
ResponseFromJava extends MappedResponse = MappedResponse,
ResponseToJava extends ResponseInterface = Partial<ResponseFromJava>
> {
responseProcessor: (request: RequestFromJava, response: ResponseFromJava) => ResponseToJava
}

export type UserKey = `user:${string}:${string}`;
export type GroupKey = `group:${string}:${string}`;
export type RoleKey = `role:${string}`;
Expand Down Expand Up @@ -263,7 +596,13 @@ export interface TextComponent {
export type Component<
Descriptor extends ComponentDescriptor = LayoutDescriptor | PageDescriptor | PartDescriptor,
Config extends NestedRecord = NestedRecord,
Regions extends Record<string, Region> = Record<string, Region>,
Regions extends (
Descriptor extends LayoutDescriptor
? Record<string, Region<(FragmentComponent | PartComponent | TextComponent)[]>>
: Record<string, Region>
) = Descriptor extends LayoutDescriptor
? Record<string, Region<(FragmentComponent | PartComponent | TextComponent)[]>>
: Record<string, Region>,
> =
| FragmentComponent
| LayoutComponent<Descriptor, Config, Regions>
Expand Down Expand Up @@ -293,6 +632,7 @@ export type Region<
Components extends
(FragmentComponent | LayoutComponent | PartComponent | TextComponent)[] =
(FragmentComponent | Layout | Part | TextComponent)[]
// @ts-expect-error TODO LayoutRegion can't eat LayoutComponent nor Layout!!!
> = PageRegion<Components> | LayoutRegion<Components>;

export interface Content<
Expand Down
Loading