UNPKG

@serenity-is/corelib

Version:
549 lines (479 loc) 19.4 kB
import { isAssignableFromSymbol, isInstanceOfTypeSymbol } from "./symbols"; import { StringLiteral, TypeInfo, ensureTypeInfo, getTypeNameProp, getTypeRegistry, globalObject, interfaceIsAssignableFrom, internalRegisterType, merge, peekTypeInfo } from "./system-internal"; export { getTypeNameProp, getTypeRegistry, setTypeNameProp, type StringLiteral } from "./system-internal"; export const nsSerenity: "Serenity." = "Serenity."; export const SerenityNS: "Serenity" = "Serenity"; declare global { interface SymbolConstructor { readonly typeInfo: unique symbol; } } /** * Get the global object (window in browsers, global in node) */ export function getGlobalObject(): any { return globalObject; } /** * Omit undefined properties from an object. Does not modify the original object. * This is useful when using Object.assign to avoid overwriting existing values with undefined * just like jQuery $.extend does. * @param x Object to omit undefined properties from * @returns New object without undefined properties */ export function omitUndefined(x: { [key: string]: any }) { if (x == null) return x; let obj = Object.create(null); Object.entries(x).forEach(([key, value]) => value !== void 0 && (obj[key] = value)); return obj; } /** * Type alias for a function or object (enum). */ export type Type = Function | Object; /** * Get a nested property from an object. Can be used to get nested properties from global object for example by separating names with dots. * @param from Object to get the property from * @param name Name of the property (dot-separated for nested properties) * @returns Value of the property or null if not found */ export function getNested(from: any, name: string) { var a = name.split('.'); for (var i = 0; i < a.length; i++) { from = from[a[i]]; if (from == null) return null; } return from; } /** * Get a type by name from the type registry, global object or a specific target. * @param name Name of the type * @param target Target object to search in (defaults to global object) * @returns The type or null if not found */ export function getType(name: string, target?: any): Type { var type: any; if (target == null) { type = getTypeRegistry()[name]; if (type != null || globalObject == void 0 || name === "Object") return type; target = globalObject; } type = getNested(target, name) if (typeof type !== 'function') return null; return type; } /** * Get the full name of a type (including namespace if any). * This returns the name from typeInfo.typeName if available (e.g. registered via decorators), * otherwise tries to get the name from function's name property. * @param type Type to get the name of */ export function getTypeFullName(type: Type): string { return getTypeNameProp(type) || (type as any).name || (type.toString().match(/^\s*function\s*([^\s(]+)/) || [])[1] || 'Object'; }; /** * Get the short name of a type (without namespace). * @param type Type to get the name of * @returns Short name of the type */ export function getTypeShortName(type: Type): string { var fullName = getTypeFullName(type); var bIndex = fullName?.indexOf('['); var nsIndex = fullName?.lastIndexOf('.', bIndex >= 0 ? bIndex : fullName.length); return nsIndex > 0 ? fullName.substring(nsIndex + 1) : fullName; }; /** * Get the instance type of an object. * @param instance Object to get the instance type of * @returns The instance type or Object if not found */ export function getInstanceType(instance: any): any { if (instance == null) throw "Can't get instance type of null or undefined!"; // Have to catch as constructor cannot be looked up on native COM objects try { return instance.constructor; } catch (ex) { return Object; } }; /** * Check if a type is assignable from another type. A type is * assignable from another type if they are the same or if the other type * is derived from it. This also works for interfaces if they are registered * via registerInterface function or decorators. * @param target Target type or interface * @param fromType Type to check assignability from * @returns true if target is assignable from type */ export function isAssignableFrom(target: any, fromType: Type) { if (target === fromType || (fromType as any).prototype instanceof target) return true; if (typeof target[isAssignableFromSymbol] === 'function') return target[isAssignableFromSymbol](fromType); return false; } /** * Check if an instance is of a specific type. * @param instance Object to check * @param type Type to check against * @returns true if instance is of type */ export function isInstanceOfType(instance: any, type: Type) { if (instance == null) return false; if (typeof (type as any)[isInstanceOfTypeSymbol] === 'function') return (type as any)[isInstanceOfTypeSymbol](instance); return isAssignableFrom(type, getInstanceType(instance)); } /** * Get the base type of a class or interface. * @param type Type to get the base type of * @returns The base type or null if not found */ export function getBaseType(type: any) { if (type == null || type === Object || !type.prototype || (type as any)[Symbol.typeInfo]?.typeKind === "interface") return null; return Object.getPrototypeOf(type.prototype).constructor; } /** * Register a class with the type system. * @param type Class type to register * @param name Name to register the class under * @param intf Optional interfaces the class implements */ export function registerClass(type: any, name: string, intf?: any[]): void { internalRegisterType(type, name, intf, "class"); } /** * Register an enum with the type system. * @param enumType Enum type to register * @param name Name to register the enum under * @param enumKey Optional key to use for the enum */ export function registerEnum(enumType: any, name: string, enumKey?: string) { if (typeof enumType !== "object" || enumType == null) throw "Enum type is required in registerEnum!"; if (name && name.endsWith(".")) throw "Enum name cannot end with a dot in registerEnum!"; internalRegisterType(enumType, name, undefined, "enum"); if (enumKey && enumKey != name) { const typeStore = getTypeRegistry(); if (!typeStore[enumKey]) typeStore[enumKey] = enumType; } } /** * Register an interface with the type system. There is no runtime representation of interfaces * in JavaScript, so Serenity uses classes decorated with some special symbols to emulate * interfaces to some degree. This is used by the type system to support isAssignableFrom and * isInstanceOfType functions for interfaces. * @param type Interface type to register * @param name Name to register the interface under * @param intf Optional interfaces the interface class implements */ export function registerInterface(type: any, name: string, intf?: any[]) { internalRegisterType(type, name, intf, "interface"); Object.defineProperty(type, isAssignableFromSymbol, { value: interfaceIsAssignableFrom, configurable: true }); } /** * Enum utilities */ export namespace Enum { /** * Convert an enum value to a string containing enum names. * @param enumType Enum type * @param value Enum value */ export let toString = (enumType: any, value: number): string => { if (value == null) return ""; if (typeof value !== "number") return "" + value; var values = enumType; if (value === 0 || !peekTypeInfo(enumType)?.enumFlags) { for (var i in values) { if (values[i] === value) { return i; } } return value == null ? "" : value.toString(); } else { var parts: string[] = []; for (var i in values) { if (typeof values[i] !== "number") continue; if (values[i] & value) { parts.push(i); value -= values[i]; } } if (value != 0) parts.push(value.toString()); return parts.join(' | '); } }; /** * Get all numeric values of an enum as an array. * @param enumType * @returns */ export let getValues = (enumType: any) => { var parts = []; var values = enumType; for (var i in values) { if (Object.prototype.hasOwnProperty.call(values, i) && typeof values[i] === "number") parts.push(values[i]); } return parts; }; } /** * Check if a type is an enum. A type is considered an enum if it is not a function * and it has isInterfaceTypeSymbol property set to null (which is set by registerEnum function). * @param type Type to check * @returns True if the type is an enum */ export const isEnum = (type: any) => { return typeof type === "object" && (type[Symbol.typeInfo] as TypeInfo<string>)?.typeKind == "enum"; }; /** * Initialize a form type. This is used in the XYZForm.ts files that are generated * by the Serenity server typings code generator. It defines getters that call this.w() to * initialize form fields on the prototype of a form class. * @param typ Form type to initialize * @param nameWidgetPairs Array of name-widget pairs */ export function initFormType(typ: Function, nameWidgetPairs: any[]) { for (var i = 0; i < nameWidgetPairs.length - 1; i += 2) { (function (name: string, widget: any) { Object.defineProperty(typ.prototype, name, { get: function () { return this.w(name, widget); }, enumerable: true, configurable: true }); })(nameWidgetPairs[i], nameWidgetPairs[i + 1]); } } const _fieldsProxy = new Proxy({}, { get: (_, p) => p }) as any; /** * Get a proxy for form fields. This proxy returns the field name for any property * accessed on it. This is used in form initialization to avoid having to declare * a variable for the fields type. There is no actual runtime check for field names, * so it is only used to provide intellisense and compile-time checks. * @returns A readonly record of form field names and same string values */ export function fieldsProxy<TRow>(): Readonly<Record<keyof TRow, string>> { return _fieldsProxy } /** * Check if an object is array-like. An object is considered array-like if it is * not null, is of type object, has a numeric length property and does not have * a nodeType property (to exclude DOM nodes). * @param obj Object to check * @returns True if the object is array-like */ export function isArrayLike(obj: any): obj is ArrayLike<any> { return typeof obj === "object" && obj != null && typeof obj.length === "number" && typeof obj.nodeType !== "number"; } /** * Check if an object is Promise-like, meaning it is either a native Promise * or an object with then and catch methods (like jQuery Deferred). * @param obj Object to check * @returns True if the object is Promise-like */ export function isPromiseLike(obj: any): obj is PromiseLike<any> { return obj instanceof Promise || ((typeof obj === "object" && obj != null && typeof obj.then === "function" && typeof obj.catch === "function")); } /** * Utility type to prevent type inference in generic types. * TypeScript 5.4 has added a built-in NoInfer<T> type that can be used instead of this. */ export type SNoInfer<T> = [T][T extends any ? 0 : never]; /** * Attribute class for editors. This is used by the editorTypeInfo function * and registerEditor function to add EditorAttribute to editors. */ export class EditorAttribute { } registerClass(EditorAttribute, 'Serenity.EditorAttribute'); /** * Marker interface for SlickGrid formatters. */ export class ISlickFormatter { } registerInterface(ISlickFormatter, 'Serenity.ISlickFormatter'); /** * Register a SlickGrid formatter. * @param type Formatter type * @param name Formatter name * @param intf Optional interface(s) to implement */ export function registerFormatter(type: any, name: string, intf?: any[]): void { registerClass(type, name, merge([ISlickFormatter], intf)); } /** * Register an editor type. Adds EditorAttribute if not already present. * @param type Editor type * @param name Editor name * @param intf Optional interface(s) to implement */ export function registerEditor(type: any, name: string, intf?: any[]) { registerClass(type, name, intf); const typeInfo = peekTypeInfo(type); typeInfo.typeKind = "editor"; if (!typeInfo.customAttributes?.some(x => getInstanceType(x) === x)) addCustomAttribute(type, new EditorAttribute()); } /** * Adds a custom attribute to a type. JavaScript does not have built-in support for attributes, * so Serenity uses a customAttributes array on typeInfo to store them. This is used by * decorators and some helper functions to add attributes to classes. * @param type * @param attr */ export function addCustomAttribute(type: any, attr: any) { let typeInfo = ensureTypeInfo(type); if (!typeInfo.customAttributes) typeInfo.customAttributes = [attr]; else typeInfo.customAttributes.push(attr); } /** * Get a custom attribute of a type. * @param type Type to get the attribute from * @param attrType Attribute type to get * @param inherit Indicates whether to search in base types * @returns The custom attribute or null if not found */ export function getCustomAttribute<TAttr>(type: any, attrType: { new(...args: any[]): TAttr }, inherit: boolean = true): TAttr { if (!type || attrType == null) return null; do { let attrs = peekTypeInfo(type)?.customAttributes; if (attrs) { for (var i = attrs.length - 1; i >= 0; i--) { let attr = attrs[i]; if (attr != null && isInstanceOfType(attr, attrType)) return attr; } } } while (inherit && (type = getBaseType(type))) } /** * Get whether a type has a specific custom attribute. * @param type Type to check * @param attrType Attribute type to check * @param inherit Indicates whether to search in base types * @returns True if the type has the attribute */ export function hasCustomAttribute<TAttr>(type: any, attrType: { new(...args: any[]): TAttr }, inherit: boolean = true): boolean { return getCustomAttribute(type, attrType, inherit) != null; } /** * Get all custom attributes of a type. * @param type Type to get the attributes from * @param attrType Attribute type to get. If not specified, all attributes are returned. * @param inherit Indicates whether to search in base types * @returns An array of custom attributes */ export function getCustomAttributes<TAttr>(type: any, attrType: { new(...args: any[]): TAttr }, inherit: boolean = true): TAttr[] { if (!type) return []; const allTypes = attrType === void 0; var result: any[] = []; do { let attrs = peekTypeInfo(type)?.customAttributes; if (attrs) { for (var i = attrs.length - 1; i >= 0; i--) { let attr = attrs[i]; if (attr && (allTypes || (attrType && isInstanceOfType(attr, attrType)))) { result.push(attr); } } } } while (inherit && (type = getBaseType(type))); return result; }; export type { TypeInfo } from "./system-internal"; /** Class type information. This is used to make type name available in declaration files unlike decorators that does not show in .d.ts files. */ export type ClassTypeInfo<TypeName> = TypeInfo<TypeName>; /** Editor type information. This is used to make type name available in declaration files unlike decorators that does not show in .d.ts files. */ export type EditorTypeInfo<TypeName> = TypeInfo<TypeName>; /** Formatter type information. This is used to make type name available in declaration files unlike decorators that does not show in .d.ts files. */ export type FormatterTypeInfo<TypeName> = TypeInfo<TypeName>; /** Interface type information. This is used to make type name available in declaration files unlike decorators that does not show in .d.ts files. */ export type InterfaceTypeInfo<TypeName> = TypeInfo<TypeName>; export function classTypeInfo<TypeName>(typeName: StringLiteral<TypeName>, intfAndAttr?: any[]): ClassTypeInfo<TypeName> { const typeInfo: TypeInfo<TypeName> = { typeKind: "class", typeName } const interfaces = intfAndAttr?.filter(x => typeof (x) === "function"); if (interfaces?.length) typeInfo.interfaces = interfaces; const attrs = intfAndAttr?.filter(x => typeof (x) !== "function") if (attrs?.length) typeInfo.customAttributes = attrs; return typeInfo; } export function editorTypeInfo<TypeName>(typeName: StringLiteral<TypeName>, intfAndAttr?: any[]): EditorTypeInfo<TypeName> { const typeInfo: TypeInfo<TypeName> = { typeKind: "editor", typeName }; const interfaces = intfAndAttr?.filter(x => typeof (x) === "function"); if (interfaces?.length) typeInfo.interfaces = interfaces; typeInfo.customAttributes = merge([new EditorAttribute()], intfAndAttr?.filter(x => typeof (x) !== "function" && x.prototype !== EditorAttribute.prototype)) return typeInfo; } export function formatterTypeInfo<TypeName>(typeName: StringLiteral<TypeName>, intfAndAttr?: any[]): FormatterTypeInfo<TypeName> { const typeInfo: TypeInfo<TypeName> = { typeKind: "formatter", typeName, interfaces: merge([ISlickFormatter], intfAndAttr?.filter(x => typeof (x) === "function")) }; const attrs = intfAndAttr?.filter(x => typeof (x) !== "function"); if (attrs?.length) typeInfo.customAttributes = attrs; return typeInfo; } export function interfaceTypeInfo<TypeName>(typeName: StringLiteral<TypeName>, intfAndAttr?: any[]): InterfaceTypeInfo<TypeName> { const typeInfo: TypeInfo<TypeName> = { typeKind: "interface", typeName } const interfaces = intfAndAttr?.filter(x => typeof (x) === "function"); if (interfaces?.length) typeInfo.interfaces = interfaces; const attrs = intfAndAttr?.filter(x => typeof (x) !== "function") if (attrs?.length) typeInfo.customAttributes = attrs; return typeInfo; } export function registerType(type: { [Symbol.typeInfo]: TypeInfo<any>, name: string }) { if (!type) throw "registerType is called with null target!"; // peekTypeInfo should auto handle registration let typeInfo: TypeInfo<any> = peekTypeInfo(type); if (!typeInfo) throw `registerType is called on type "${type.name}" that does not have a static [Symbol.typeInfo] property!`; if (!typeInfo.typeName) throw `registerType is called on type "${type.name}", but it's typeInfo property does not have a typeName!`; } export interface TransformInclude { }