Skip to content

Instantly share code, notes, and snippets.

@renoirb
Last active April 15, 2021 22:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renoirb/94e405016b18a6757d8484d3f72c131f to your computer and use it in GitHub Desktop.
Save renoirb/94e405016b18a6757d8484d3f72c131f to your computer and use it in GitHub Desktop.
Type Guarded limited Map of named Data Transfer Object
# http://editorconfig.org
# Match with prettier.config.js AND .gitattributes
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.{aspx,cmd,config,cs,csproj,ps1,rels,resx,sln}]
end_of_line = clrf
node_modules/
*.swp
package-lock.json
dist/

Type Guarded limited Map of named Data Transfer Object

Imagine we have a test suite, in that test suite we want to run scenarios against a deployed application that has accounts configured.

That deployed application has a number of accounts, and they are configured for possible real-world possibilities.

That we want to run that test suite either locally from our own development setup, and from a Continous Integration step.

Our company's security team might ask us NOT TO commit passwords in the code base.

The test suite might run from different users locally, or from any other deployment (e.g. UAT, production, etc).

Where we might have a Continous Integration utility where we store secrets, and where they become available as Continous Integration shell Environment variables.

In the test suite, we want to be able to tell to run a scenario for a given user (e.g. userWithProfilePicture)

So we need a way to tell from the test case which user to use, and "trust" that the proper username and password be used.

Over time, in a big team, we might add more than two users, and we might want to run tests not only on UAT, but maybe on production.

So we need systems:

  1. To make it easy in tests to say which user to use
  2. To fill up each possible test accounts's username and password
  3. To support loading from either a file, or a shell environent
  4. Have the warranty that ONLY and ALL users has values, and are non empty

So that:

  • Each test-case has code completion and helpers
  • Test-case fails ONLY WHEN the actual feature fails, NOT when there's missing data

Typical implementation scenario

Say we have a known number of keys (e.g. 'userWithProfilePicture', 'userWithProfilePicture') that must exist, where each entry has a Data Transfer Object of a specific shape (e.g. {username: string, password: string}), and that they can't be with empty values (e.g. '' is unacceptable).

Instead of using TypeScript enum where in the end would make an Object (see constant enum members), let's leverage ECMAScript Map.

When we use non constant enum, we get pretty much an object where each key and values reverselookup

export enum FooTestingUserNames {
  userWithProfilePicture = 'userWithProfilePicture',
  userTwoKey = 'userTwoKey',
}

Becomes

export var FooTestingUserNames
;(function (FooTestingUserNames) {
  FooTestingUserNames['userWithProfilePicture'] = 'userWithProfilePicture'
  FooTestingUserNames['userTwoKey'] = 'userTwoKey'
})(FooTestingUserNames || (FooTestingUserNames = {}))

Where if we would use const enum FooTestingUserNames {}, would have no output at all.

Most of code base omit or forgets that we can have const enums (even the author of this note, by the way!).

Symptom is still that the object FooTestingUserNames from which we want to get the value from it (e.g. FooTestingUserNames.userWithProfilePicture) as a way to select from another hash map by from an object that can be mutated at run time is a risk for an unexpected state.

The main idea of having FooTestingUser.userWithProfilePicture is that we want to know that FooTestingUser can either pick userWithProfilePicture OR userTwoKey.

That’s useful for type checking and switching, but it is contrived and not always easy to properly check.

From those keys (userWithProfilePicture OR userTwoKey), we want an object shape that might look like this Record<'username' | 'password', string>

export type IAccountCredential = Record<'username' | 'password', string>

export enum FooTestingUserNames {
  userWithProfilePicture = 'userWithProfilePicture',
  userTwoKey = 'userTwoKey',
}

export const FooTestingUser = {
  userWithProfilePicture: {
    username: 'user1@localhost',
    password: 'password1',
  },
  userTwoKey: { username: 'user2@localhost', password: 'password2' },
}

But we might often see colleagues forget to type which FooTestingUser keys can and MUST only contain defined entries

So we'd manually add typing Record<FooTestingUserNames, IAccountCredential> in a review comment.

export const FooTestingUser: Record<FooTestingUserNames, IAccountCredential> = {
  userWithProfilePicture: {
    username: 'user1@localhost',
    password: 'password1',
  },
  userTwoKey: { username: 'user2@localhost', password: 'password2' },
}

Which is fine, because if we try to add userThreeKey we get an error message which contains;

export const FooTestingUser: Record<FooTestingUserNames, IAccountCredential> = {
  userWithProfilePicture: {
    username: 'user1@localhost',
    password: 'password1',
  },
  userTwoKey: { username: 'user2@localhost', password: 'password2' },
  userThreeKey: { username: 'user3@localhost', password: 'password3' },
  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Type '{ ... userThreeKey: { ... }; }' is not assignable to type 'Record<FooTestingUserNames, IAccountCredential>'.
  // Object literal may only specify known properties, and 'userThreeKey' does not exist in type 'Record<FooTestingUserNames, IAccountCredential>'
}

Type '{ userThreeKey: { username: string; password: string; }; }' is not assignable to type 'Record<FooTestingUserNames, IAccountCredential>'

In other words userThreeKey is not in the known list of FooTestingUserNames and that is a good thing. But, at that is type checking, not enforced during run time.

Also, those values can't be changed dynamically from a JSON file, or from shell environment.

Typically in a company, the Continuous Integration system is shared, and for good and for bac, we all have to deal with its particularities.

We could manually implement that scenario by hand specifically for one company's project, and let other teams re-implement that problem.

How would an ideal solution look like?

The ideal solution should be re-usable, and if there are missing or unexpected situation, to fail before starting the test run.

The concerns are at 3 levels:

  1. We can’t know if one project uses exactly the same "user" types, it has to be configurable

    • Ability to describe the possible users that must exist
    • Ability to tell which Shell environment variable names to use for each possible users and fields
    • Ability to tell which Input file should be read from, should it exist
  2. Detect and Load the data

    • Load from a data source, and only continue if all criterias are met
      • If the first one failed, carry on with the other one
      • If non passed, throw an exception
    • Only one of the following is acceptable;
      • If there are Shell environment variables, it we should read from it fist
      • If the Input file is present, load, parse its contents, and ensure it fills all
  3. Expose available possible users

    • Ability to just say FooTestingUser.userWithProfilePicture in the code, and the test suite code will get the appropriate data.

To work on that scenario, we can write up functionaly based on example above, where some recurring situations occurs, they're expanded in the following notes:

Proposal

/**
 * The Data Transfer Object containing the current values for both keys
 * username and password.
 */
export type IAccountCredential = Record<'username' | 'password', string>

/**
 * When we want to pass an unknown object, best normalize so we can use it for picking values.
 */
export interface INormalizedHashMap {
  readonly discriminant: 'NormalizedHashMap'
  [key: string]: string
}

/**
 * What `create` returns is a contextualized manager.
 * That manager should make sure it loads data
 * and ensure it throws errors if something unexpected
 */
export interface ITestingAccountsCredentialMarshaller<
  N extends Readonly<string>,
  K extends N[number]
> {
  /**
   * true when all credentials are set
   */
  readonly loaded: boolean
  /**
   * List of user name keys
   */
  readonly TEST_ACCOUNTS: ReadonlySet<K>
  /**
   * When we have to pick key and values for each account from
   * process.env, we got to know for which user and keys they are
   * going to be mapped for.
   */
  createProcessEnvPickerMap(): Map<K, IAccountCredential | null>
  getCredential(name: K): IAccountCredential
  /**
   * Define a credential map for an user name (keys)
   */
  setCredential(name: K, accountCredential: IAccountCredential): boolean
  isAccountName(name: string): name is K
  loadFromNormalized(
    normalized: INormalizedHashMap,
    pickerMap: Map<K, IAccountCredential | null>,
  ): boolean
  toHashMap(): Readonly<Record<K, IAccountCredential>>
}

Using

It should look like this

// File: where-we-define-things.ts

import {
  createTestingAccountsCredentialMarshaller,
  normalizeHashMap,
} from 'Type-Guarded-limited-Map-of-named-Data-Transfer-Object'

/**
 * To enforce which users MUST exist, tell their names in this array
 *
 * So that we can organize how to load values for them.
 */
export const THIS_APPLICATION_TEST_USERS = createTestingAccountsCredentialMarshaller(
  ['userWithProfilePicture'],
)

/**
 * This is to say;
 *
 * For this app, we know in CI mode, we'll have each user with their credentials coming
 * from process.env.<key>, here is the mapping.
 *
 * If:
 * - The system detects any missing account names ("userWithProfilePicture"), it will throw an exception.
 * - During CI mode, we don't get value for any of those names, it will throw an exception
 * - Any of those fails, we'll trust the data will be loaded in another way
 */
const processEnvPickerMap = THIS_APPLICATION_TEST_USERS.createProcessEnvPickerMap()
processEnvPickerMap.set('userWithProfilePicture', {
  // Say we have a CI with process.env.username_for_userWithProfilePicture for userWithProfilePicture’s username value
  username: 'username_for_userWithProfilePicture',
  password: 'password_for_userWithProfilePicture',
})

/**
 * This is the object from which we’ll know from the tests which account names are availables.
 */
export type IFooTestingUser = ReturnType<
  typeof THIS_APPLICATION_TEST_USERS.toHashMap
>

export const tryLoadPromProcessEnv = (): IFooTestingUser => {
  const normalized = normalizeHashMap(process.env)
  THIS_APPLICATION_TEST_USERS.loadFromNormalized(
    normalized,
    processEnvPickerMap,
  )
  // If any keys missing (e.g. we have another entry in the initial array from create above) it would fail here
  const FooTestingUser = THIS_APPLICATION_TEST_USERS.toHashMap()
  // For now, but maybe to be reworked, this is how we make sure all is there and fine
  return FooTestingUser
}

export const tryLoadFromOtherMethod = (): IFooTestingUser => {
  // If it worked from tryLoadPromProcessEnv, this should fail ASAP and not mutate.
  /**
   * Really, this should be done in some way from a JSON file and fill the values.
   * But, for this example, let's do by hand.
   */
  THIS_APPLICATION_TEST_USERS.setCredential('userWithProfilePicture', {
    username: 'user1@localhost',
    password: 'password1',
  })
  // If any keys missing (e.g. we have another entry in the initial array from create above) it would fail here
  const FooTestingUser = THIS_APPLICATION_TEST_USERS.toHashMap()
  return FooTestingUser
}

Then from another file, say a test suite helper

// File: some-helper.ts

import { Helper } from 'codeceptjs'

import {
  tryLoadPromProcessEnv,
  tryLoadFromOtherMethod,
  THIS_APPLICATION_TEST_USERS,
} from './where-we-define-things'

export class SomeHelper extends Helper {
  _init(): void {
    try {
      // Whichever, works first, but only one will.
      // In any case,
      // The job of those actions is to fill `THIS_APPLICATION_TEST_USERS`'s _internal
      // Map with their respective IAccountCredential values set.
      // It is also to make sure only and all `TEST_ACCOUNTS` "account names" are present,
      // with non empty values.
      tryLoadPromProcessEnv()
      // Try load from elsewhere too here
      tryLoadFromOtherMethod()
      // Maybe those should throw two exeptions
      // - Everything is already set (no need to continue)
      // - Something we need to know and fail for.
    } catch (_) {
      /* */
    }
  }

  getUser: ReturnType<typeof THIS_APPLICATION_TEST_USERS.getCredential> = (
    testUser,
  ) => {
    // Once loaded, from whichever data source, first that succeeded,
    // we can delegate getting values using getCredential
    const credentialMap = THIS_APPLICATION_TEST_USERS.getCredential(testUser)
    return credentialMap
  }
}
/* eslint-disable @typescript-eslint/naming-convention */
// -------------------------------- BEGIN: Paste into README.md --------------------------------
/**
* The Data Transfer Object containing the current values for both keys
* username and password.
*/
export type IAccountCredential = Record<'username' | 'password', string>
/**
* When we want to pass an unknown object, best normalize so we can use it for picking values.
*/
export interface INormalizedHashMap {
readonly discriminant: 'NormalizedHashMap'
[key: string]: string
}
/**
* What `create` returns is a contextualized manager.
* That manager should make sure it loads data
* and ensure it throws errors if something unexpected
*/
export interface ITestingAccountsCredentialMarshaller<
N extends Readonly<string>,
K extends N[number]
> {
/**
* true when all credentials are set
*/
readonly loaded: boolean
/**
* List of user name keys
*/
readonly TEST_ACCOUNTS: ReadonlySet<K>
/**
* When we have to pick key and values for each account from
* process.env, we got to know for which user and keys they are
* going to be mapped for.
*/
createProcessEnvPickerMap(): Map<K, IAccountCredential | null>
getCredential(name: K): IAccountCredential
/**
* Define a credential map for an user name (keys)
*/
setCredential(name: K, accountCredential: IAccountCredential): boolean
isAccountName(name: string): name is K
loadFromNormalized(
normalized: INormalizedHashMap,
pickerMap: Map<K, IAccountCredential | null>,
): boolean
toHashMap(): Readonly<Record<K, IAccountCredential>>
}
// --------------------------------- END: Paste into README.md ---------------------------------
// ----------------------------------- IMPLEMENTATION SKETCH -----------------------------------
/**
* Utility method to take an unknown object,
* and normalize so we get only a HashMap with key and values as strings.
*
* The input may as well be from a part of a parsed JSON file or process.env
*/
export type INormalizeHashMapFn = (payload: unknown) => INormalizedHashMap
const fallbackValues: IAccountCredential = {
username: '',
password: '',
}
/**
* Either use Object.defineProperty or Proxy
* I'd prefer Proxy, but that works too
*
* Bookmarks:
* - https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Proxy
* - https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
*/
// @ts-ignore
const forKey = <K extends unknown>(
map: Map<K, IAccountCredential>,
k: string,
): PropertyDescriptor => {
const accountCredential = map.has(k as K)
? map.get(k as K)
: ({} as IAccountCredential)
const value: IAccountCredential = {
...fallbackValues,
...accountCredential,
}
const descriptor: PropertyDescriptor = {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze(value),
}
return descriptor
}
const withValue = (
accountCredential: IAccountCredential,
): PropertyDescriptor => {
if (!accountCredential.username || !accountCredential.password) {
const empty = Object.entries(accountCredential)
.map(([field, value]) => (!value ? field : undefined))
.filter(Boolean)
let message = `E0: We are missing required values for fields ${empty.join(
', ',
)}`
throw new Error(message)
}
const value: IAccountCredential = {
...fallbackValues,
...accountCredential,
}
const descriptor: PropertyDescriptor = {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze(value),
}
return descriptor
}
export const isObject = <T = Record<string, unknown>>(
value: unknown,
): value is T => {
// https://github.com/lodash/lodash/blob/4.17.21-es/isObject.js
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ca8d73db/types/lodash.isobject/index.d.ts
const type = typeof value
return value != null && (type == 'object' || type == 'function')
}
export const normalizeHashMap: INormalizeHashMapFn = (payload) => {
if (isObject(payload)) {
const normalized = Object.create(null) as INormalizedHashMap
const descriptor: PropertyDescriptor = {
enumerable: true,
writable: false,
configurable: false,
value: 'NormalizedHashMap',
}
Reflect.defineProperty(normalized, 'discriminant', descriptor)
for (const [key, value] of Object.entries(payload)) {
if (typeof key === 'string' && typeof value === 'string') {
const descriptor: PropertyDescriptor = {
enumerable: true,
writable: false,
configurable: false,
value: Object.freeze(value),
}
Reflect.defineProperty(normalized, key, descriptor)
}
}
return normalized
} else {
const message = `E11: Invalid input, we expected an object`
throw new Error(message)
}
}
/**
* To enforce which users MUST exist, tell their names in this array
*
* So that we can organize how to load values for them.
*
* To use, we first have to create a contextualized
* configuration manager for the project we're importing it for.
*/
export const createTestingAccountsCredentialMarshaller = <
N extends Readonly<string>
>(
names: N[],
): ITestingAccountsCredentialMarshaller<N, typeof names[number]> => {
let _loaded = false
const TEST_ACCOUNT_NAMES = Object.freeze(
[...names].filter((k) => typeof k === 'string'),
)
const TEST_ACCOUNT_NAMES_LIST = TEST_ACCOUNT_NAMES.join(', ')
const TEST_ACCOUNTS: ReadonlySet<typeof names[number]> = new Set([
...TEST_ACCOUNT_NAMES,
])
const _internal = new Map<typeof names[number], IAccountCredential>()
/**
* Pass-in keys we've found, throw an error
*/
const maybeThrowMissingKeysError = (found: Set<string>) => {
const missing = TEST_ACCOUNT_NAMES.filter(
(mustHaveKey) => !found.has(mustHaveKey),
)
const mustBeZero = missing.length
if (mustBeZero > 0) {
const missingAccountNames = missing.join(', ')
const message = `
E1: We expected to have ${mustBeZero} user accounts mapped,
but we are missing for: ${missingAccountNames}
`
throw new Error(message)
}
}
const setCredential: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['setCredential'] = (name, accountCredential) => {
if (!isAccountName(name)) {
const message = `E2: Unexpected user name "${name}", it is not part of ${TEST_ACCOUNT_NAMES_LIST}`
throw new Error(message)
}
if (_internal.has(name) === false) {
_internal.set(name, Object.freeze({ ...accountCredential }))
return true
} else {
const message = `E3: We already have values for user name "${name}"`
throw new Error(message)
}
}
const getCredential: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['getCredential'] = (name) => {
if (!_internal.has(name)) {
const message = `E4: There are no credential configured for user name "${name}"`
throw new Error(message)
}
const { username = '', password = '' } = _internal.get(
name,
) as IAccountCredential
if (!username || !password) {
const message = `E5: Unexpected situation, account "${name}", has empty values`
throw new Error(message)
}
const accountCredential: IAccountCredential = {
username,
password,
}
return accountCredential
}
const isAccountName: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['isAccountName'] = (name: string): name is typeof names[number] => {
return TEST_ACCOUNTS.has(name as typeof names[number])
}
const createProcessEnvPickerMap: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['createProcessEnvPickerMap'] = () => {
const out = new Map<typeof names[number], IAccountCredential | null>()
const entries = Array.from(TEST_ACCOUNTS)
for (const userKey of entries) {
if (isAccountName(userKey)) {
out.set(userKey, null)
}
}
return out
}
const toHashMap: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['toHashMap'] = () => {
const _found = new Set<string>()
const hashMap: Record<
typeof names[number],
IAccountCredential
> = Object.create(null)
const entries = Array.from(TEST_ACCOUNTS)
for (const userKey of entries) {
try {
const dto = getCredential(userKey)
Reflect.defineProperty(hashMap, userKey, withValue(dto))
_found.add(userKey)
} catch (e) {
const message = `E6: ${e.message}`
throw new Error(message)
}
}
maybeThrowMissingKeysError(_found)
return Object.freeze(hashMap)
}
const loadFromNormalized: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
>['loadFromNormalized'] = (normalized, pickerMap) => {
const _found = new Set<string>()
if (pickerMap.size < 1) {
const message = `E7: We should only work on a non empty map`
throw new Error(message)
}
if (pickerMap.size !== TEST_ACCOUNTS.size) {
const message = `E8: We should only work on a map with ${TEST_ACCOUNTS.size} entries, we only have ${pickerMap.size} found`
throw new Error(message)
}
const entries = Array.from(pickerMap)
for (const [credentialKey, processEnvKey] of entries) {
if (!isAccountName(credentialKey)) {
const message = `E9: Unexpected user name "${credentialKey}", it is not part of ${TEST_ACCOUNT_NAMES_LIST}`
throw new Error(message)
}
if (processEnvKey) {
const username = Reflect.has(normalized, processEnvKey.username)
? Reflect.get(normalized, processEnvKey.username)
: null
const password = Reflect.has(normalized, processEnvKey.password)
? Reflect.get(normalized, processEnvKey.password)
: null
const mapped: IAccountCredential = {
username,
password,
}
_internal.set(credentialKey, mapped)
_found.add(credentialKey)
}
}
if (_internal.size === TEST_ACCOUNTS.size) {
// Everything's loaded and should be fine
_loaded = true
} else {
maybeThrowMissingKeysError(_found)
}
return _loaded
}
const marshall: ITestingAccountsCredentialMarshaller<
N,
typeof names[number]
> = {
TEST_ACCOUNTS,
get loaded(): boolean {
return _loaded
},
createProcessEnvPickerMap,
getCredential,
isAccountName,
loadFromNormalized,
setCredential,
toHashMap,
}
return marshall
}
import {
createTestingAccountsCredentialMarshaller,
normalizeHashMap,
} from './impl'
export const THIS_APPLICATION_TEST_USERS = createTestingAccountsCredentialMarshaller(
['userWithProfilePicture', 'userTwoKey'],
)
/**
* This is to say;
*
* For this app, we know in CI mode, we'll have each user with their credentials coming
* from process.env.<key>, here is the mapping.
*
* If:
* - The system detects any missing account names ("userWithProfilePicture"), it will throw an exception.
* - During CI mode, we don't get value for any of those names, it will throw an exception
* - Any of those fails, we'll trust the data will be loaded in another way
*/
const processEnvPickerMap = THIS_APPLICATION_TEST_USERS.createProcessEnvPickerMap()
processEnvPickerMap.set('userWithProfilePicture', {
username: 'username_for_userWithProfilePicture',
password: 'password_for_userWithProfilePicture',
})
processEnvPickerMap.set('userTwoKey', {
username: 'username_key_for_two',
password: 'password_key_for_two',
})
/**
* Tell which process.env.<key> to use for each properties
*/
export const tryLoadPromProcessEnv = (): IFooTestingUser => {
const normalized = normalizeHashMap(process.env)
THIS_APPLICATION_TEST_USERS.loadFromNormalized(
normalized,
processEnvPickerMap,
)
// If any keys missing (e.g. we have another entry in the initial array from create above) it would fail here
const FooTestingUser = THIS_APPLICATION_TEST_USERS.toHashMap()
// For now, but maybe to be reworked, this is how we make sure all is there and fine
return FooTestingUser
}
/**
* This is the object from which we’ll know from the tests which account names are availables.
*/
export type IFooTestingUser = ReturnType<
typeof THIS_APPLICATION_TEST_USERS.toHashMap
>
export const tryLoadFromOtherMethod = (): IFooTestingUser => {
// If it worked from tryLoadPromProcessEnv, this should fail ASAP and not mutate.
/**
* Really, this should be done in some way from a JSON file and fill the values.
* But, for this example, let's do by hand.
*/
THIS_APPLICATION_TEST_USERS.setCredential('userWithProfilePicture', {
username: 'user1@localhost',
password: 'password1',
})
THIS_APPLICATION_TEST_USERS.setCredential('userTwoKey', {
username: 'user2@localhost',
password: 'password2',
})
// If any keys missing (e.g. we have another entry in the initial array from create above) it would fail here
const FooTestingUser = THIS_APPLICATION_TEST_USERS.toHashMap()
return FooTestingUser
}
{
"name": "Type-Guarded-limited-Map-of-named-Data-Transfer-Object",
"private": true,
"scripts": {
"format": "conventions-use-prettier --write '**.{ts,md}'",
"play": "nodemon -e ts -w . -x ts-node impl.ts",
"build": "tsc --build tsconfig.json"
},
"devDependencies": {
"@renoirb/conventions-use-prettier": "^1.3.0",
"@renoirb/conventions-use-typescript-3": "^1.2.0",
"nodemon": "^2.0.0",
"@types/node": "^14.14.0",
"ts-node": "^9.1.0",
"typescript": "^4.0.0",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=14"
}
}
{
"extends": "./node_modules/@renoirb/conventions-use-typescript-3/includes/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"diagnostics": true,
"strict": true,
"module": "ESNext",
"target": "ES2019",
"types": ["node"],
"lib": ["ES2019", "dom"]
},
"include": [
"*.ts"
]
}
@renoirb
Copy link
Author

renoirb commented Mar 11, 2021

Screen Shot 2021-03-10 at 9 40 14 PM

Screen Shot 2021-03-10 at 9 41 07 PM

Screen Shot 2021-03-10 at 9 45 26 PM

@renoirb
Copy link
Author

renoirb commented Mar 11, 2021

To be discussed

Should we let test run to show passwords at all?

Screen Shot 2021-03-11 at 16 51 24

@renoirb
Copy link
Author

renoirb commented Apr 15, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment