4.0.0 • Published 2 years ago

@zthun/works.core v4.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

Description

This is the main data contract package for @zthun scoped projects. It contains a collection of interfaces and object builders which represents different structures throughout the Zthunworks system.

Installation

# NPM
npm install @zthun/works.core
# Yarn
yarn add @zthun/works.core

Usage

All data contracts are divided into separate categories of usage, but the overwhelming majority of them follow the same structure and pattern.

Builders

Builder Pattern

Zthunworks heavily makes use of the builder pattern to construct objects instead of Typescript classes since the overwhelming majority of these objects are meant to be sent over the internet. By using a builder, we can retrieve a plain javascript object which will not lose its prototype when serializing and deserializing. All builders are implicit and do not implement a given interface. Instead, they follow a common pattern described below.

type ZBuilder<T> = {
  /**
   * Returns the built object.
   *
   * This should be a copy of the built object and not the internal object being built.
   *
   * @returns A deep copy of the built object.
   */
  build: () => T;

  /**
   * Copies an already built object into the builder to compose with later.
   *
   * @param other The object to deep copy.  This can be a shallow copy if other only has primitive types.
   *
   * @returns The builder object.
   */
  copy?: (other: T) => this;

  /**
   * Takes a partial object implementation of T and assigns the set properties to the builder object.
   *
   * @param other The object to assign to the builder object.
   *
   * @returns The builder object.
   */
  assign?: (other: Partial<T>) => this;
};

Configuration

Configuration Vault

A configuration entry in the Zthunworks system is represented by the IZConfigEntry interface and uses the ZConfigEntryBuilder object to construct them. These come back from the vault database and are used to hold system configuration that a normal user will not ever see.

A configuration entry is divided into 3 parts. The configuration scope, the key, and the value. Keys must be unique in their given scope, but you can have the same named key in different scopes.

Best practice: The scope is the system configuration for the application that is currently running and the key is the name of the configuration. There should always be a root scope called default or common which all applications will access for shared configuration.

The following document list is an example of a vault database that may exist with configuration entries:

[
  {
    "scope": "common",
    "key": "domain",
    "value": "zthunworks"
  },
  {
    "scope": "common",
    "key": "log-level",
    "value": "ERROR"
  },
  {
    "scope": "foo-app",
    "key": "retries",
    "value": 5
  }
]

Here, the vault has three configurations, with two common scopes and one for foo-app. While any app could access any configuration, it would be best for foo-app to only access configurations under common and foo-app. This forces a good separation between application configuration, but still provides a central repository for configuration.

import { IZConfigEntry, ZConfigEntryBuilder } from '@zthun/works.core';

function createDefaultDomainConfig(): IZConfigEntry {
  const config: IZConfigEntry = new ZConfigEntryBuilder().scope('common').key('domain').value('zthunworks').build();
  return config;
}

Email

Email Messages

Emails are a common way to notify users of upcoming site news, but in the Zthunworks system, they are used to validate accounts and help recover lost passwords.

Email objects are split into three categories: the email itself, the email envelope, and the individual contacts involved with the envelope.

  • The main email is represented by the IZEmail interface and uses the ZEmailBuilder to construct it.
  • The email envelope is represented by the IZEmailEnvelope and uses the ZEmailEnvelopeBuilder to construct it.
  • The email contact is represented IZEmailContact and uses the ZEmailContactBuilder to construct it.
import { IZEmail, IZEmailEnvelope, IZEmailContact, ZEmailBuilder, ZEmailEnvelopeBuilder, ZEmailContactBuilder } from '@zthun/works.core';

function createEmailMessageToAdmin(msg: string, subject: string) {
  const current: IZEmailContact = getCurrentUserEmailAddress();
  const admin = new ZEmailContactBuilder().address('admin@zthunworks.com').display('Admin').type('email-message').build();
  const envelope: IZEmailEnvelope = new ZEmailEnvelopeBuilder().from(current).to(admin).build();
  const email: IZEmail = new ZEmailBuilder().envelope(envelope).message(msg).subject(subject).build();
  return email;
}

Errors

Error Handling

Dealing with errors server side is common, but the main issue is localization. When you have an error happen on the server, it is best to just send back an error code instead of a raw text message. This way, when the error is returned to the user, it is translated according to the culture zone that the user is currently in. The standard error object is the IZError interface and the ZErrorBuilder is used to construct it.

At minimum, errors should have a code, and this can be the standard HTTP error codes that are returned to browsers. Optionally, errors may have the type, a sub-code or list of sub-codes that further describes what happened, and an english friendly message or array of messages that describe the code or sub-codes. Why english, and not spanish or other languages? It's because the developer who developed this only speaks english and he needed a way to quickly identify errors that came back from the server. Technically, this can be any language you want it to be in, and you can use it is the primary display message, but it is recommended not to.

HTTP error codes are conveniently located for you using the ZHttpCode* enums.

/**
 * Details codes 100-199
 */
export enum ZHttpCodeInformationalResponse {}

/**
 * Details codes 200-299
 */
export enum ZHttpCodeSuccess {}

/**
 * Details codes 300-399
 */
export enum ZHttpCodeRedirection {}

/**
 * Details codes 400-499
 */
export enum ZHttpCodeClient {}

/**
 * Details codes 500-599
 */
export enum ZHttpCodeServer {}

The following example details usage for the ZErrorBuilder.

import { IZError, ZErrorBuilder } from '@zthun/works.core';

function createErrorForUnhandledException(msg: string, type: string) {
  const error = new ZErrorBuilder(ZHttpCodeServer.InternalServerError).type(type).english(msg).build();
  return error;
}

Authentication

User Manipulation

Managing users and auth is divided into three separate types.

  • The IZLogin is constructed with the ZLoginBuilder and is used to send credentials for a user not logged in, or for those users that do not currently have an existing account. This object should NEVER be saved in the database.
  • The IZProfile is constructed with the ZProfileBuilder and it contains the semi-private information for the user that can be modified by said user. This object should NEVER be saved in the database.
  • The IZUser is constructed with the ZUserBuilder and contains all the public, semi-private, and private information. This object is stored in the database and should NEVER be sent to a client. If you need to strip the semi-private information from this object, use the ZProfileBuilder and use the user method.
import { IZUser, IZProfile, ZProfileBuilder } from '@zthun/works.core';

function createProfileFromUser(user: IZUser): IZProfile {
  return new ZProfileBuilder().user(user).build();
}

Data

Data Handling

Very often, you need to sort and filter data to display it to the user.

For sorting, we have the IZSort and the ZSortBuilder for construction. A sort is divided into two parts: the field to be sorted, and the direction to sort. Unlike most builders, the ZSortBuilder will output a list of IZSort objects for multi-sort support.

import { IZSort, ZSortBuilder } from '@zthun/works.core';

function sortByNameThenByAge() {
  const sort: IZSort[] = new ZSortBuilder().ascending('name').descending('age').build();
  return sort;
}

Filters are a bit more complicated. Filters don't have a single root interface; instead, they have a composite type that is made up of multiple interface options.

The IZFilter can be any of the following filter types with a respective builder for each type.

/**
 * Used for x, y comparisons.
 *
 * @see ZBinaryFilterBuilder for creation.
 *
 * @example x EQUALS y
 */
export interface IZBinaryFilter {}

/**
 * Used to check x against a list of y.
 *
 * @see ZCollectionFilterBuilder for creation.
 *
 * @example x IN y.
 */
export interface IZCollectionFilter {}

/**
 * A composite list of filters x, y, z, etc, joined by a logic clause.
 *
 * @see ZLogicFilterBuilder for creation.
 *
 * @example x AND y
 */
export interface IZLogicFilter {}

/**
 * A filter involving a single clause, x.
 *
 * @see ZUnaryFilterBuilder for creation.
 *
 * @example x IS NOT NULL
 */
export interface IZUnaryFilter {}

The following example shows the general use of creating a logical filter clause by using builders.

import { IZBinaryFilter, IZLogicFilter, IZCollectionFilter, IZUnaryFilter, ZCollectionFilterBuilder, ZBinaryFilter, ZLogicFilter, ZUnaryFilter } from '@zthun/works.core';

function createComplexFilter(): IZFilter {
  const ageIsSet: IZUnaryFilter = new ZUnaryFilterBuilder().field('age').isNotNull().build();
  const ageIsAdult: IZBinaryFilter = new ZBinaryFilterBuilder().field('age').greaterThanEqualTo().value(0).build();
  const ageIsNotTwentyOneOrTwentyFive: IZCollectionFilter = new ZCollectionFilterBuilder().field('age').notIn().values([21, 25]).build();
  const filter: IZLogicFilter = new ZLogicFilterBuilder().and().clause(ageIsSet).clause(ageIsAdult).clause(ageIsNotTwentyOneOrTwentyFive).build();
  return filter;
}

Server

Server Identification

You will often need to work with remote systems. Luckily, there is the IZServer interface with the ZServerBuilder to construct it.

At minimum, you will need the address to connect to. Additionally, unless the target system is completely open and public on the default port, given the protocol, you will need to fill out the information for the port, username, and password in order to make a valid connection to a remote system. This object is especially useful if your system uses other services like smtp email servers and ftp hosts.

import { IZServer, ZServerBuilder } from '@zthun/works.core';

function createFtpServerConnection() {
  const server: IZServer = new ZServerBuilder().address('ftp://my-host.info').port(8922).username('foo').password('pa$$w0rD').build();
  return server;
}

Typedoc

Typedoc

Zthunworks treats Typedoc documentation as a first class citizen and is able to load typedoc json files into memory for analysis. Rather than using the classes found in the actual typedoc github package, @zthun/works.core just defines the base interfaces and enums for a very small footprint. This lets you load a remote json object into memory and can quickly access the members with full intellisense support.

import { IZTypedoc } from '@zthun/works.core';
import Axios from 'axios';

async function readTypedoc(url: string): Promise<IZTypedoc> {
  const res = await Axios.get<IZTypedoc>(url);
  return res.data;
}
4.0.0

2 years ago

2.3.0

2 years ago

2.2.0

2 years ago

2.0.0

2 years ago

3.0.2

2 years ago

3.0.0

2 years ago

1.2.0

3 years ago

1.1.0

3 years ago

1.0.0

3 years ago

1.0.0-29

3 years ago

1.0.0-28

3 years ago

1.0.0-27

3 years ago

1.0.0-26

3 years ago

1.0.0-24

3 years ago

1.0.0-22

3 years ago

1.0.0-21

3 years ago

1.0.0-19

4 years ago

1.0.0-17

4 years ago

1.0.0-18

4 years ago

1.0.0-16

4 years ago

1.0.0-15

4 years ago

1.0.0-14

4 years ago

1.0.0-13

4 years ago

1.0.0-12

4 years ago

1.0.0-11

4 years ago

1.0.0-9

4 years ago

1.0.0-7

4 years ago

1.0.0-6

4 years ago