@afora/core 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/core/application/types/application.interface.d.ts +56 -11
- package/dist/core/blueprint/types/slice.interface.d.ts +5 -2
- package/dist/core/command/command.d.ts +11 -4
- package/dist/core/command/types/command.interface.d.ts +19 -0
- package/dist/core/entity/abstract-entity.d.ts +1 -1
- package/dist/core/entity/types/entity.interface.d.ts +13 -7
- package/dist/core/exception/exception.d.ts +10 -1
- package/dist/core/output/status.d.ts +4 -3
- package/dist/core/property/input.d.ts +20 -0
- package/dist/core/property/property.d.ts +4 -4
- package/dist/core/property/types/optional-property.type.d.ts +2 -2
- package/dist/core/property/types/property-type.interface.d.ts +17 -0
- package/dist/core/record/abstract-record.d.ts +4 -4
- package/dist/core/record/exceptions.d.ts +2 -0
- package/dist/core/record/name.d.ts +16 -0
- package/dist/core/record/plugin.d.ts +6 -2
- package/dist/core/record/record.d.ts +4 -3
- package/dist/core/record/types/plugin.interface.d.ts +11 -0
- package/dist/core/record/types/record.interface.d.ts +71 -10
- package/dist/core/repository/cursor.d.ts +13 -0
- package/dist/core/repository/exceptions.d.ts +10 -0
- package/dist/core/repository/query.d.ts +4 -4
- package/dist/core/repository/repository.d.ts +2 -2
- package/dist/core/repository/state.d.ts +9 -0
- package/dist/core/repository/types/query.interface.d.ts +37 -0
- package/dist/core/repository/types/repository.interface.d.ts +17 -6
- package/dist/core/use-case/exceptions.d.ts +36 -0
- package/dist/core/use-case/plugin-catalog.d.ts +11 -0
- package/dist/core/use-case/plugin.d.ts +25 -0
- package/dist/core/use-case/steps/types/steps.interface.d.ts +24 -7
- package/dist/core/use-case/target.d.ts +9 -0
- package/dist/core/use-case/types/plugin.interface.d.ts +185 -0
- package/dist/experimental.js +4 -3
- package/dist/{index-8m80wjzf.js → index-q6hggpzw.js} +1210 -85
- package/dist/{index-hn82hm31.js → index-r78dsvtc.js} +136 -66
- package/dist/index.d.ts +22 -12
- package/dist/index.js +47 -5
- package/dist/std/command.d.ts +11 -0
- package/dist/std/properties/password.d.ts +21 -2
- package/dist/std/record-plugins.d.ts +5 -5
- package/dist/std/use-cases/auth.d.ts +5 -0
- package/dist/std/use-cases/crud.d.ts +23 -0
- package/dist/std/use-cases/index.d.ts +11 -0
- package/dist/std/use-cases/lifecycle.d.ts +13 -0
- package/dist/std/use-cases/shared.d.ts +60 -0
- package/dist/std/use-cases/types.d.ts +212 -0
- package/dist/testing/index.js +99 -2
- package/dist/testing/provider-contract.d.ts +5 -0
- package/package.json +1 -1
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { type IPropertyInput, PropertyInput } from "../../core/property/input.js";
|
|
2
3
|
export declare const PASSWORD_MIN_LENGTH = 7;
|
|
3
4
|
export declare const PASSWORD_MAX_LENGTH = 128;
|
|
4
5
|
export declare const PASSWORD_SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|\\;:'\",.<>/?`~";
|
|
5
6
|
export declare function isPasswordHash(value: string): boolean;
|
|
6
7
|
/** The rules a plain password breaks; empty when it is strong enough. */
|
|
7
8
|
export declare function passwordIssues(value: string): string[];
|
|
8
|
-
declare const
|
|
9
|
+
declare const PlainPassword_base: import("../../index.js").PropertyClass<Type.TString, string> & {
|
|
10
|
+
readonly type: {
|
|
11
|
+
readonly sensitive: true;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A password as a person types it, checked against the password rules
|
|
16
|
+
* (`passwordIssues`); sensitive. It is what an input takes for a `Password`,
|
|
17
|
+
* which stores the hash.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PlainPassword extends PlainPassword_base {
|
|
20
|
+
}
|
|
21
|
+
declare const Password_base: import("../../index.js").PropertyClass<Type.TString, string> & {
|
|
22
|
+
readonly type: {
|
|
23
|
+
readonly sensitive: true;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
9
26
|
/**
|
|
10
27
|
* A password stored as an Argon2 hash. The constructor only accepts a hash,
|
|
11
28
|
* e.g. one read from the database; `Password.hash` turns a plain password
|
|
12
|
-
* into one.
|
|
29
|
+
* into one. Under `PropertyInput` it says so to what derives inputs: they
|
|
30
|
+
* take a `PlainPassword` and hash it.
|
|
13
31
|
*/
|
|
14
32
|
export declare class Password extends Password_base {
|
|
33
|
+
static readonly [PropertyInput]: IPropertyInput<string>;
|
|
15
34
|
/** Validates `plain` against the password rules and hashes it with the configured hasher (argon2id by default). */
|
|
16
35
|
static hash(plain: string): Promise<Password>;
|
|
17
36
|
/** Whether `input` is the password this hash was made from. */
|
|
@@ -6,7 +6,7 @@ declare const PublishableRecord_base: import("../index.js").RecordConstructor<im
|
|
|
6
6
|
safeParse(value: unknown): import("../index.js").ParseResult<string | undefined>;
|
|
7
7
|
readonly "~standard": import("../index.js").StandardSchemaV1.Props<string | undefined, string | undefined>;
|
|
8
8
|
}>;
|
|
9
|
-
}>, [], never, unknown>;
|
|
9
|
+
}>, [], never, unknown, never, never>;
|
|
10
10
|
/**
|
|
11
11
|
* A `publishedAt` absent while the record is a draft; `publish(at?)` sets it
|
|
12
12
|
* (to now by default), `unpublish()` clears it, `isPublished` tells.
|
|
@@ -30,7 +30,7 @@ declare const ArchivableRecord_base: import("../index.js").RecordConstructor<imp
|
|
|
30
30
|
safeParse(value: unknown): import("../index.js").ParseResult<string | undefined>;
|
|
31
31
|
readonly "~standard": import("../index.js").StandardSchemaV1.Props<string | undefined, string | undefined>;
|
|
32
32
|
}>;
|
|
33
|
-
}>, [], never, unknown>;
|
|
33
|
+
}>, [], never, unknown, never, never>;
|
|
34
34
|
/**
|
|
35
35
|
* An `archivedAt` absent while the record is active; `archive(at?)` sets it,
|
|
36
36
|
* `restore()` clears it, `isArchived` tells.
|
|
@@ -49,7 +49,7 @@ export declare class ArchivableRecord extends ArchivableRecord_base {
|
|
|
49
49
|
export declare const Archivable: import("../index.js").IRecordPlugin<typeof ArchivableRecord, "archivable">;
|
|
50
50
|
declare const TaggableRecord_base: import("../index.js").RecordConstructor<import("../core/property/types/property-options.interface.js").StoredFields<{
|
|
51
51
|
tags: import("../index.js").IBlueprintList<import("../index.js").PropertyClass<import("typebox").TString, string>, true>;
|
|
52
|
-
}>, [], never, unknown>;
|
|
52
|
+
}>, [], never, unknown, never, never>;
|
|
53
53
|
/**
|
|
54
54
|
* `tags`, a list of strings, each once; `tag`, `untag` and `hasTag` change and read it.
|
|
55
55
|
*
|
|
@@ -67,7 +67,7 @@ export declare class TaggableRecord extends TaggableRecord_base {
|
|
|
67
67
|
export declare const Taggable: import("../index.js").IRecordPlugin<typeof TaggableRecord, "taggable">;
|
|
68
68
|
declare const OwnableRecord_base: import("../index.js").RecordConstructor<import("../core/property/types/property-options.interface.js").StoredFields<{
|
|
69
69
|
ownerId: import("../index.js").CallableProperty<typeof import("../index.js").Id>;
|
|
70
|
-
}>, [], never, unknown>;
|
|
70
|
+
}>, [], never, unknown, never, never>;
|
|
71
71
|
/**
|
|
72
72
|
* An `ownerId`, the id of who owns the record, with `isOwnedBy` and `transferTo`.
|
|
73
73
|
*
|
|
@@ -84,7 +84,7 @@ export declare const Ownable: import("../index.js").IRecordPlugin<typeof Ownable
|
|
|
84
84
|
declare const AuthenticatableRecord_base: import("../index.js").RecordConstructor<import("../core/property/types/property-options.interface.js").StoredFields<{
|
|
85
85
|
email: import("../index.js").CallableProperty<import("../index.js").PropertyClass<import("typebox").TString, string>>;
|
|
86
86
|
password: import("../index.js").CallableProperty<typeof Password>;
|
|
87
|
-
}>, readonly ["password"], "email", unknown>;
|
|
87
|
+
}>, readonly ["password"], "email", unknown, never, never>;
|
|
88
88
|
/**
|
|
89
89
|
* An `email`, unique, and a `password` hash, sensitive, changed by
|
|
90
90
|
* `change.password` and checked by `verifyPassword`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoginMaker, RegisterMaker } from "./types.js";
|
|
2
|
+
/** Stores an authenticatable entity, unless its email is taken. */
|
|
3
|
+
export declare const register: import("../../index.js").IUseCasePlugin<"register", RegisterMaker>;
|
|
4
|
+
/** Gives the authenticatable entity of an email, when the password matches. */
|
|
5
|
+
export declare const login: import("../../index.js").IUseCasePlugin<"login", LoginMaker>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UseCaseCompilation, UseCaseTargetInfo } from "../../core/use-case/types/plugin.interface.js";
|
|
2
|
+
import type { AnyExceptionClass } from "../../core/use-case/use-case.js";
|
|
3
|
+
import { type Options, type Step } from "./shared.js";
|
|
4
|
+
import type { CreateMaker, DeleteMaker, GetMaker, ListMaker, RestoreMaker, UpdateMaker } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The declaration of `create` and of `register`, which adds `before` and
|
|
7
|
+
* its errors: an input of the keys of the entity, the entity built from it
|
|
8
|
+
* and stored.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createCompilation(target: UseCaseTargetInfo, options: Options, verb: string, before?: readonly Step[], errors?: readonly AnyExceptionClass[]): UseCaseCompilation;
|
|
11
|
+
/** Builds an entity from the input and stores it. */
|
|
12
|
+
export declare const create: import("../../index.js").IUseCasePlugin<"create", CreateMaker>;
|
|
13
|
+
/** Gives the entity of an id. */
|
|
14
|
+
export declare const get: import("../../index.js").IUseCasePlugin<"get", GetMaker>;
|
|
15
|
+
/** Gives a page of entities. */
|
|
16
|
+
export declare const list: import("../../index.js").IUseCasePlugin<"list", ListMaker>;
|
|
17
|
+
/** Changes the keys the input gives, of those the entity lets change. */
|
|
18
|
+
export declare const update: import("../../index.js").IUseCasePlugin<"update", UpdateMaker>;
|
|
19
|
+
/** Deletes the entity of an id, as the repository deletes: for good, or softly. */
|
|
20
|
+
declare const remove: import("../../index.js").IUseCasePlugin<"delete", DeleteMaker>;
|
|
21
|
+
export { remove as delete_ };
|
|
22
|
+
/** Brings back a soft-deleted entity. */
|
|
23
|
+
export declare const restore: import("../../index.js").IUseCasePlugin<"restore", RestoreMaker>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IUseCaseCatalog } from "../../core/use-case/types/plugin.interface.js";
|
|
2
|
+
import type { StdUseCasePlugins } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The standard use cases, by name, for the `useCases` of `commandOf` to
|
|
5
|
+
* pick from, as `(ctx) => ({ create: ctx.create(Posts) })`: `create`, `get`,
|
|
6
|
+
* `list`, `update`, `delete` and `restore` for any entity; `publish`,
|
|
7
|
+
* `unpublish`, `archive`, `unarchive`, `tag` and `untag` for the entities
|
|
8
|
+
* with those record plugins; `register` and `login` for the
|
|
9
|
+
* authenticatable ones. `StdUseCases.context` is that `ctx`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const StdUseCases: IUseCaseCatalog<StdUseCasePlugins>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ArchiveMaker, PublishMaker, TagMaker } from "./types.js";
|
|
2
|
+
/** Publishes the entity of an id. */
|
|
3
|
+
export declare const publish: import("../../index.js").IUseCasePlugin<"publish", PublishMaker>;
|
|
4
|
+
/** Makes the entity of an id a draft again. */
|
|
5
|
+
export declare const unpublish: import("../../index.js").IUseCasePlugin<"unpublish", PublishMaker>;
|
|
6
|
+
/** Archives the entity of an id. */
|
|
7
|
+
export declare const archive: import("../../index.js").IUseCasePlugin<"archive", ArchiveMaker>;
|
|
8
|
+
/** Makes the archived entity of an id active again. */
|
|
9
|
+
export declare const unarchive: import("../../index.js").IUseCasePlugin<"unarchive", ArchiveMaker>;
|
|
10
|
+
/** Adds tags to the entity of an id. */
|
|
11
|
+
export declare const tag: import("../../index.js").IUseCasePlugin<"tag", TagMaker>;
|
|
12
|
+
/** Removes tags from the entity of an id. */
|
|
13
|
+
export declare const untag: import("../../index.js").IUseCasePlugin<"untag", TagMaker>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BlueprintField } from "../../core/blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { UseCaseCompilation, UseCaseFieldPlan, UseCaseTargetInfo } from "../../core/use-case/types/plugin.interface.js";
|
|
3
|
+
import type { AnyExceptionClass } from "../../core/use-case/use-case.js";
|
|
4
|
+
/** A blueprint of the fields given, with no context of properties. */
|
|
5
|
+
export declare const fields: import("../../index.js").BlueprintBuilder<readonly []>;
|
|
6
|
+
export type Options = Readonly<Record<string, unknown>>;
|
|
7
|
+
export type Step = Readonly<Record<string, unknown>>;
|
|
8
|
+
export declare const capitalize: (key: string) => string;
|
|
9
|
+
/** A value given in options as a step takes it: a string that starts with `$` is not a reference. */
|
|
10
|
+
export declare function literal(value: unknown): unknown;
|
|
11
|
+
/** The option `key`, checked to be one of the `allowed` options of the use case. */
|
|
12
|
+
export declare function checkOptions(target: UseCaseTargetInfo, options: Options, allowed: readonly string[]): void;
|
|
13
|
+
/** The name, description, tags and `deprecated` of the use case. */
|
|
14
|
+
export declare function metaOf(target: UseCaseTargetInfo, options: Options, verb: string): Pick<UseCaseCompilation, "name" | "description" | "tags" | "deprecated">;
|
|
15
|
+
/** The key of the response that holds the entity: the option `as`, `item` by default. */
|
|
16
|
+
export declare function asOf(target: UseCaseTargetInfo, options: Options): string;
|
|
17
|
+
/** The error of `kind` the options give, else `fallback`. */
|
|
18
|
+
export declare function errorOf(target: UseCaseTargetInfo, options: Options, kind: "notFound" | "conflict" | "invalid" | "owner", fallback: AnyExceptionClass): AnyExceptionClass;
|
|
19
|
+
/** The list of names an option gives, each a key of the entity. */
|
|
20
|
+
export declare function keysOf(target: UseCaseTargetInfo, options: Options, option: string): readonly UseCaseFieldPlan[];
|
|
21
|
+
/** The object an option gives, by key of the entity. */
|
|
22
|
+
export declare function valuesOf(target: UseCaseTargetInfo, options: Options, option: string): Readonly<Record<string, unknown>>;
|
|
23
|
+
/** Who owns the entity, as the option `owner` says. */
|
|
24
|
+
export interface Owner {
|
|
25
|
+
readonly plan: UseCaseFieldPlan;
|
|
26
|
+
/** The key of the context that holds the id of the caller. */
|
|
27
|
+
readonly context: string;
|
|
28
|
+
/** The field of that context key: an id. */
|
|
29
|
+
readonly field: BlueprintField;
|
|
30
|
+
/** The reference to the id of the owner of the entity bound as `item`. */
|
|
31
|
+
readonly reference: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The option `owner: { key?, context }`: `key` (`ownerId` by default) holds
|
|
35
|
+
* the owner, an id or an entity, and `context` names the context key with
|
|
36
|
+
* the id of the caller.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ownerOf(target: UseCaseTargetInfo, options: Options): Owner | undefined;
|
|
39
|
+
/** The context blueprint an owner needs, if there is one. */
|
|
40
|
+
export declare function contextOf(owner: Owner | undefined): Pick<UseCaseCompilation, "context">;
|
|
41
|
+
/**
|
|
42
|
+
* The steps that load the entity of `$input.id` as `item`, throw `notFound`
|
|
43
|
+
* when there is none, and when it is not the caller's.
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadSteps(owner: Owner | undefined, notFound: AnyExceptionClass): Step[];
|
|
46
|
+
/** The id field of the entity of `target`. */
|
|
47
|
+
export declare const idOf: (target: UseCaseTargetInfo) => BlueprintField;
|
|
48
|
+
/** The classes of `errors`, each once. */
|
|
49
|
+
export declare const distinct: (...errors: readonly AnyExceptionClass[]) => AnyExceptionClass[];
|
|
50
|
+
/**
|
|
51
|
+
* A use case over one entity found by id: `{ id }` in, `{ [as]: entity }`
|
|
52
|
+
* out; loads it, checks the owner, runs `steps` (which leave the entity to
|
|
53
|
+
* give under `result`) and gives it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function entityUseCase(target: UseCaseTargetInfo, options: Options, verb: string, allowed: readonly string[], steps: (context: {
|
|
56
|
+
readonly owner: Owner | undefined;
|
|
57
|
+
}) => {
|
|
58
|
+
readonly steps: readonly Step[];
|
|
59
|
+
readonly result: string;
|
|
60
|
+
}): UseCaseCompilation;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { AnyBlueprint, AnyBlueprintList, AnyRecordClass, BlueprintField, IBlueprint, IBlueprintCollection, IBlueprintList, OptionalBlueprint, OptionalBlueprintList, OptionalRecord } from "../../core/blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { EntityIdentityKey } from "../../core/entity/types/entity.interface.js";
|
|
3
|
+
import type { OptionalProperty } from "../../core/property/types/optional-property.type.js";
|
|
4
|
+
import type { AnyPropertyConstructor } from "../../core/property/types/property-collection.type.js";
|
|
5
|
+
import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf } from "../../core/repository/types/repository.interface.js";
|
|
6
|
+
import type { IUseCasePlugin, UseCaseTarget } from "../../core/use-case/types/plugin.interface.js";
|
|
7
|
+
import type { AnyExceptionClass, StepsUseCaseConstructor } from "../../core/use-case/use-case.js";
|
|
8
|
+
import type { Integer, StringProperty } from "../properties/primitives.js";
|
|
9
|
+
type EntityOf<Repository> = Repository extends {
|
|
10
|
+
readonly entity: infer Entity extends AnyRepositoryEntity;
|
|
11
|
+
} ? Entity : never;
|
|
12
|
+
type PropertiesOf<Entity> = Entity extends {
|
|
13
|
+
readonly blueprint: IBlueprint<infer Properties extends IBlueprintCollection>;
|
|
14
|
+
} ? Properties : never;
|
|
15
|
+
type Props<Repository> = PropertiesOf<EntityOf<Repository>>;
|
|
16
|
+
type FieldAt<Properties, Key extends string> = Properties extends {
|
|
17
|
+
readonly [Name in Key]: infer Field extends BlueprintField;
|
|
18
|
+
} ? Field : never;
|
|
19
|
+
type IdField<Repository> = FieldAt<Props<Repository>, "id">;
|
|
20
|
+
type KeyOf<Repository> = keyof Props<Repository> & string;
|
|
21
|
+
/** Whether a field holds an entity, one that may be absent, or a list of them. */
|
|
22
|
+
type HoldsEntity<Field> = Field extends AnyRepositoryEntity ? true : Field extends {
|
|
23
|
+
readonly record: AnyRepositoryEntity;
|
|
24
|
+
} ? true : Field extends IBlueprintList<infer Item> ? Item extends AnyRepositoryEntity ? true : false : false;
|
|
25
|
+
/** The field that accepts `undefined` too, as `optionalOf` gives it. */
|
|
26
|
+
type Optional<Field> = Field extends AnyPropertyConstructor ? OptionalProperty<Field> : Field extends AnyBlueprintList ? OptionalBlueprintList<Field> : Field extends AnyBlueprint ? OptionalBlueprint<Field> : Field extends AnyRecordClass ? OptionalRecord<Field> : never;
|
|
27
|
+
/** The errors a standard use case throws, in place of its defaults. */
|
|
28
|
+
export interface StdUseCaseErrors {
|
|
29
|
+
/** For an entity that is not found, or is not the caller's. `UseCaseEntityNotFound` by default. */
|
|
30
|
+
readonly notFound?: AnyExceptionClass;
|
|
31
|
+
/** For an entity that already exists (`register`). `UseCaseEntityConflict` by default. */
|
|
32
|
+
readonly conflict?: AnyExceptionClass;
|
|
33
|
+
/** For credentials that do not match (`login`). `UseCaseInvalidCredentials` by default. */
|
|
34
|
+
readonly invalid?: AnyExceptionClass;
|
|
35
|
+
/** For a caller that is not found, when it owns what `create` makes. `notFound` by default. */
|
|
36
|
+
readonly owner?: AnyExceptionClass;
|
|
37
|
+
}
|
|
38
|
+
/** The options every standard use case takes. */
|
|
39
|
+
export interface StdUseCaseOptions {
|
|
40
|
+
/** The `useCaseName`; the verb and the name of the entity by default, as `CreatePost`. */
|
|
41
|
+
readonly name?: string;
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
readonly tags?: readonly string[];
|
|
44
|
+
readonly deprecated?: boolean;
|
|
45
|
+
readonly errors?: StdUseCaseErrors;
|
|
46
|
+
}
|
|
47
|
+
/** Who owns the entities: the caller, whose id the context gives. */
|
|
48
|
+
export interface StdOwnerOption<Repository> {
|
|
49
|
+
/**
|
|
50
|
+
* `key` holds the owner, as an id or as an entity (`ownerId` by default,
|
|
51
|
+
* the key of the `ownable` plugin), and `context` names the context key
|
|
52
|
+
* with the id of the caller: `create` fills the key from it, a use case
|
|
53
|
+
* over one entity throws `notFound` for another owner's, and `list` gives
|
|
54
|
+
* only the caller's.
|
|
55
|
+
*/
|
|
56
|
+
readonly owner?: {
|
|
57
|
+
readonly key?: KeyOf<Repository>;
|
|
58
|
+
readonly context: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The options of a standard use case that gives one entity. */
|
|
62
|
+
export interface StdEntityOptions<Repository> extends StdUseCaseOptions, StdOwnerOption<Repository> {
|
|
63
|
+
/** The key of the response that holds the entity; `item` by default. */
|
|
64
|
+
readonly as?: string;
|
|
65
|
+
}
|
|
66
|
+
/** The options of `create` and `register`. */
|
|
67
|
+
export interface StdCreateOptions<Repository> extends StdEntityOptions<Repository> {
|
|
68
|
+
/** Optional keys the input does not take. */
|
|
69
|
+
readonly omit?: readonly KeyOf<Repository>[];
|
|
70
|
+
/** Values the entity is built with, in place of taking them from the input. */
|
|
71
|
+
readonly values?: {
|
|
72
|
+
readonly [Key in KeyOf<Repository>]?: unknown;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** The options of `publish`, `unpublish`, `archive` and `unarchive`. */
|
|
76
|
+
export interface StdLifecycleOptions<Repository> extends StdEntityOptions<Repository> {
|
|
77
|
+
/** Change keys set before the entity is stored, e.g. `{ status: "published" }`. */
|
|
78
|
+
readonly set?: {
|
|
79
|
+
readonly [Key in KeyOf<Repository>]?: unknown;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** The options of `list`. */
|
|
83
|
+
export interface StdListOptions<Repository> extends StdUseCaseOptions, StdOwnerOption<Repository> {
|
|
84
|
+
/** The order of the page; newest first (`createdAt`, descending) by default. */
|
|
85
|
+
readonly orderBy?: readonly {
|
|
86
|
+
readonly key: KeyOf<Repository>;
|
|
87
|
+
readonly direction?: "asc" | "desc";
|
|
88
|
+
}[];
|
|
89
|
+
}
|
|
90
|
+
type None = Record<never, never>;
|
|
91
|
+
type AsOf<Options> = Options extends {
|
|
92
|
+
readonly as: infer Key extends string;
|
|
93
|
+
} ? Key : "item";
|
|
94
|
+
type EntityResponse<Repository, Options> = {
|
|
95
|
+
readonly [Key in AsOf<Options>]: EntityOf<Repository>;
|
|
96
|
+
};
|
|
97
|
+
type OwnerContext<Repository, Options> = Options extends {
|
|
98
|
+
readonly owner: {
|
|
99
|
+
readonly context: infer Key extends string;
|
|
100
|
+
};
|
|
101
|
+
} ? {
|
|
102
|
+
readonly [Name in Key]: IdField<Repository>;
|
|
103
|
+
} : undefined;
|
|
104
|
+
type OwnerKey<Options> = Options extends {
|
|
105
|
+
readonly owner: infer Owner;
|
|
106
|
+
} ? Owner extends {
|
|
107
|
+
readonly key: infer Key;
|
|
108
|
+
} ? Key : "ownerId" : never;
|
|
109
|
+
type LeftOut<Options> = EntityIdentityKey | OwnerKey<Options> | (Options extends {
|
|
110
|
+
readonly omit: readonly (infer Key)[];
|
|
111
|
+
} ? Key : never) | (Options extends {
|
|
112
|
+
readonly values: infer Values;
|
|
113
|
+
} ? keyof Values : never);
|
|
114
|
+
/** The input of `create`: the keys of the entity, an entity it holds by `<key>Id`. */
|
|
115
|
+
type CreateInput<Repository, Options> = {
|
|
116
|
+
readonly [Key in KeyOf<Repository> as Key extends LeftOut<Options> ? never : Props<Repository>[Key] extends AnyRepositoryEntity ? `${Key}Id` : HoldsEntity<Props<Repository>[Key]> extends true ? never : Key]: Props<Repository>[Key] extends AnyRepositoryEntity ? FieldAt<PropertiesOf<Props<Repository>[Key]>, "id"> : Props<Repository>[Key];
|
|
117
|
+
};
|
|
118
|
+
type ChangeKey<Repository> = EntityInstanceOf<EntityOf<Repository>> extends {
|
|
119
|
+
readonly change: infer Changes;
|
|
120
|
+
} ? keyof Changes & KeyOf<Repository> : never;
|
|
121
|
+
/** The input of `update`: the id, and each change key, optional. */
|
|
122
|
+
type UpdateInput<Repository> = {
|
|
123
|
+
readonly [Key in "id" | ChangeKey<Repository> as Key extends "id" ? Key : HoldsEntity<Props<Repository>[Key]> extends true ? never : Key]: Key extends "id" ? IdField<Repository> : Optional<Props<Repository>[Key]>;
|
|
124
|
+
};
|
|
125
|
+
type EntityUseCase<Repository extends AnyRepository, Options, Input extends IBlueprintCollection> = StepsUseCaseConstructor<Input, EntityResponse<Repository, Options>, {
|
|
126
|
+
readonly repository: Repository;
|
|
127
|
+
}, OwnerContext<Repository, Options>>;
|
|
128
|
+
type ById<Repository> = {
|
|
129
|
+
readonly id: IdField<Repository>;
|
|
130
|
+
};
|
|
131
|
+
/** `create`: for a repository with the `create` action. */
|
|
132
|
+
export type CreateMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "create">, options?: Options) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>>;
|
|
133
|
+
/** `get`: for a repository with `findById`. */
|
|
134
|
+
export type GetMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
|
|
135
|
+
/** `list`: for a repository with `findPage`. */
|
|
136
|
+
export type ListMaker = <Repository extends AnyRepository, const Options extends StdListOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findPage">, options?: Options) => StepsUseCaseConstructor<{
|
|
137
|
+
readonly limit: OptionalProperty<typeof Integer>;
|
|
138
|
+
readonly offset: OptionalProperty<typeof Integer>;
|
|
139
|
+
}, {
|
|
140
|
+
readonly items: IBlueprintList<EntityOf<Repository>>;
|
|
141
|
+
readonly total: typeof Integer;
|
|
142
|
+
readonly limit: typeof Integer;
|
|
143
|
+
readonly offset: typeof Integer;
|
|
144
|
+
}, {
|
|
145
|
+
readonly repository: Repository;
|
|
146
|
+
}, OwnerContext<Repository, Options>>;
|
|
147
|
+
/** `update`: for a repository with `findById` and `update`. */
|
|
148
|
+
export type UpdateMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update">, options?: Options) => EntityUseCase<Repository, Options, UpdateInput<Repository>>;
|
|
149
|
+
/** `delete`: for a repository with `findById` and `delete`. */
|
|
150
|
+
export type DeleteMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "delete">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
|
|
151
|
+
/** `restore`: for a repository with soft deletion and the `restore` action. */
|
|
152
|
+
export type RestoreMaker = <Repository extends AnyRepository, const Options extends StdUseCaseOptions & {
|
|
153
|
+
readonly as?: string;
|
|
154
|
+
} = None>(repository: Repository & UseCaseTarget<Repository, "restore">, options?: Options) => StepsUseCaseConstructor<ById<Repository>, EntityResponse<Repository, Options>, {
|
|
155
|
+
readonly repository: Repository;
|
|
156
|
+
}>;
|
|
157
|
+
/** `publish` and `unpublish`: for a publishable entity. */
|
|
158
|
+
export type PublishMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
|
|
159
|
+
publish(at?: string): unknown;
|
|
160
|
+
unpublish(): unknown;
|
|
161
|
+
}, "publishable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
|
|
162
|
+
/** `archive` and `unarchive`: for an archivable entity. */
|
|
163
|
+
export type ArchiveMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
|
|
164
|
+
archive(at?: string): unknown;
|
|
165
|
+
readonly isArchived: boolean;
|
|
166
|
+
}, "archivable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
|
|
167
|
+
/** `tag` and `untag`: for a taggable entity. */
|
|
168
|
+
export type TagMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
|
|
169
|
+
tag(...tags: string[]): unknown;
|
|
170
|
+
untag(...tags: string[]): unknown;
|
|
171
|
+
}, "taggable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository> & {
|
|
172
|
+
readonly tags: IBlueprintList<typeof StringProperty>;
|
|
173
|
+
}>;
|
|
174
|
+
type Authenticatable = {
|
|
175
|
+
verifyPassword(plain: string): Promise<boolean>;
|
|
176
|
+
};
|
|
177
|
+
/** `register`: for an authenticatable entity, with `findByEmail` and `create`. */
|
|
178
|
+
export type RegisterMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail" | "create", Authenticatable, "authenticatable">, options?: Options) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>>;
|
|
179
|
+
/** `login`: for an authenticatable entity, with `findByEmail`. */
|
|
180
|
+
export type LoginMaker = <Repository extends AnyRepository, const Options extends StdUseCaseOptions & {
|
|
181
|
+
readonly as?: string;
|
|
182
|
+
} = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail", Authenticatable, "authenticatable">, options?: Options) => StepsUseCaseConstructor<{
|
|
183
|
+
readonly email: FieldAt<Props<Repository>, "email">;
|
|
184
|
+
readonly password: typeof StringProperty;
|
|
185
|
+
}, EntityResponse<Repository, Options>, {
|
|
186
|
+
readonly repository: Repository;
|
|
187
|
+
}>;
|
|
188
|
+
/**
|
|
189
|
+
* The standard use cases, by name: what the `useCases` of `commandOf` gets
|
|
190
|
+
* to pick from, each called with a repository and its options.
|
|
191
|
+
*/
|
|
192
|
+
export interface StdUseCaseContext {
|
|
193
|
+
readonly create: CreateMaker;
|
|
194
|
+
readonly get: GetMaker;
|
|
195
|
+
readonly list: ListMaker;
|
|
196
|
+
readonly update: UpdateMaker;
|
|
197
|
+
readonly delete: DeleteMaker;
|
|
198
|
+
readonly restore: RestoreMaker;
|
|
199
|
+
readonly publish: PublishMaker;
|
|
200
|
+
readonly unpublish: PublishMaker;
|
|
201
|
+
readonly archive: ArchiveMaker;
|
|
202
|
+
readonly unarchive: ArchiveMaker;
|
|
203
|
+
readonly tag: TagMaker;
|
|
204
|
+
readonly untag: TagMaker;
|
|
205
|
+
readonly register: RegisterMaker;
|
|
206
|
+
readonly login: LoginMaker;
|
|
207
|
+
}
|
|
208
|
+
/** The plugins of `StdUseCases`, by name. */
|
|
209
|
+
export type StdUseCasePlugins = {
|
|
210
|
+
readonly [Name in keyof StdUseCaseContext]: IUseCasePlugin<Name, StdUseCaseContext[Name]>;
|
|
211
|
+
};
|
|
212
|
+
export {};
|
package/dist/testing/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import"../index-
|
|
1
|
+
import"../index-r78dsvtc.js";
|
|
2
2
|
import {
|
|
3
3
|
RepositoryUniqueViolation2,
|
|
4
4
|
RepositoryEntityNotFound2,
|
|
5
5
|
blueprintOf2,
|
|
6
6
|
entityOf2
|
|
7
|
-
} from "../index-
|
|
7
|
+
} from "../index-q6hggpzw.js";
|
|
8
8
|
|
|
9
9
|
// src/testing/provider-contract.ts
|
|
10
10
|
function contextOf(name = "Accounts") {
|
|
@@ -129,6 +129,14 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
129
129
|
await provider.update(context, { ...data, name: "Other" });
|
|
130
130
|
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual({ ...data, name: "Other" });
|
|
131
131
|
});
|
|
132
|
+
it("update clears a value that is left out", async () => {
|
|
133
|
+
const { provider, context, seed } = await fixture();
|
|
134
|
+
const data = row({ deletedAt: "2024-01-01T00:00:00.000Z" });
|
|
135
|
+
await seed(data);
|
|
136
|
+
const { deletedAt: _deletedAt, ...alive } = data;
|
|
137
|
+
await provider.update(context, alive);
|
|
138
|
+
expect(await provider.findOne(context, { where: { id: data.id } })).toEqual(alive);
|
|
139
|
+
});
|
|
132
140
|
it("soft-deleted rows keep occupying unique values", async () => {
|
|
133
141
|
const { provider, context, seed } = await fixture();
|
|
134
142
|
await seed(row({
|
|
@@ -371,6 +379,95 @@ function providerContract(runner, makeProvider, options = {}) {
|
|
|
371
379
|
expect(stored).toEqual(data);
|
|
372
380
|
});
|
|
373
381
|
});
|
|
382
|
+
describe("keyset", () => {
|
|
383
|
+
const keyset = (name, body) => it(name, async () => {
|
|
384
|
+
const enabled = options.keyset ?? (await makeProvider()).capabilities?.keyset;
|
|
385
|
+
if (enabled === true)
|
|
386
|
+
await body();
|
|
387
|
+
});
|
|
388
|
+
const seeded = async () => {
|
|
389
|
+
const f = await fixture();
|
|
390
|
+
const [a, b, c, d, e, g] = [
|
|
391
|
+
row({ n: 1, group: 1, name: "Gil" }),
|
|
392
|
+
row({ group: 1, name: "Érico" }),
|
|
393
|
+
row({ n: 2, group: 2, name: "Ana" }),
|
|
394
|
+
row({ n: 2, group: 1, name: "ana" }),
|
|
395
|
+
row({ group: 2, name: "Bia" }),
|
|
396
|
+
row({ n: 3, group: 2, name: "Zoe" })
|
|
397
|
+
];
|
|
398
|
+
await f.seed(a, b, c, d, e, g);
|
|
399
|
+
const find = (query) => f.provider.findMany(f.context, query).then(ids);
|
|
400
|
+
return { ...f, a, b, c, d, e, g, find };
|
|
401
|
+
};
|
|
402
|
+
const by = (key, direction = "asc") => [{ key, direction }, { key: "id" }];
|
|
403
|
+
keyset("gives the rows strictly after the position, ascending", async () => {
|
|
404
|
+
const { find, a, c, d, g } = await seeded();
|
|
405
|
+
const orderBy = by("n");
|
|
406
|
+
expect(await find({ orderBy, after: [1, a.id] })).toEqual([
|
|
407
|
+
c.id,
|
|
408
|
+
d.id,
|
|
409
|
+
g.id
|
|
410
|
+
]);
|
|
411
|
+
expect(await find({ orderBy, after: [2, c.id] })).toEqual([
|
|
412
|
+
d.id,
|
|
413
|
+
g.id
|
|
414
|
+
]);
|
|
415
|
+
expect(await find({ orderBy, after: [3, g.id] })).toEqual([]);
|
|
416
|
+
});
|
|
417
|
+
keyset("follows a descending order", async () => {
|
|
418
|
+
const { find, a, c, d, g } = await seeded();
|
|
419
|
+
const orderBy = by("n", "desc");
|
|
420
|
+
expect(await find({
|
|
421
|
+
orderBy,
|
|
422
|
+
after: [3, g.id],
|
|
423
|
+
where: { n: { in: [1, 2] } }
|
|
424
|
+
})).toEqual([c.id, d.id, a.id]);
|
|
425
|
+
expect(await find({
|
|
426
|
+
orderBy,
|
|
427
|
+
after: [2, d.id],
|
|
428
|
+
where: { n: { in: [1, 2] } }
|
|
429
|
+
})).toEqual([a.id]);
|
|
430
|
+
});
|
|
431
|
+
keyset("missing values are first ascending and last descending", async () => {
|
|
432
|
+
const { find, a, b, c, d, e, g } = await seeded();
|
|
433
|
+
expect(await find({ orderBy: by("n"), after: [undefined, b.id] })).toEqual([e.id, a.id, c.id, d.id, g.id]);
|
|
434
|
+
expect(await find({ orderBy: by("n"), after: [undefined, e.id] })).toEqual([a.id, c.id, d.id, g.id]);
|
|
435
|
+
expect(await find({ orderBy: by("n", "desc"), after: [1, a.id] })).toEqual([b.id, e.id]);
|
|
436
|
+
expect(await find({ orderBy: by("n", "desc"), after: [undefined, b.id] })).toEqual([e.id]);
|
|
437
|
+
expect(await find({ orderBy: by("n", "desc"), after: [undefined, e.id] })).toEqual([]);
|
|
438
|
+
});
|
|
439
|
+
keyset("compares strings by code point", async () => {
|
|
440
|
+
const { find, a, b, c, d, e, g } = await seeded();
|
|
441
|
+
expect(await find({ orderBy: by("name"), after: ["Bia", e.id] })).toEqual([a.id, g.id, d.id, b.id]);
|
|
442
|
+
expect(await find({ orderBy: by("name", "desc"), after: ["ana", d.id] })).toEqual([g.id, a.id, e.id, c.id]);
|
|
443
|
+
});
|
|
444
|
+
keyset("combines with where, withDeleted and limit; count ignores it", async () => {
|
|
445
|
+
const { provider, context, find, a, b, d } = await seeded();
|
|
446
|
+
const orderBy = [{ key: "id" }];
|
|
447
|
+
expect(await find({
|
|
448
|
+
orderBy,
|
|
449
|
+
after: [a.id],
|
|
450
|
+
where: { group: 1 },
|
|
451
|
+
limit: 1
|
|
452
|
+
})).toEqual([b.id]);
|
|
453
|
+
expect(await find({ orderBy, after: [b.id], where: { group: 1 } })).toEqual([d.id]);
|
|
454
|
+
await provider.update(context, {
|
|
455
|
+
...d,
|
|
456
|
+
deletedAt: "2024-01-01T00:00:00.000Z"
|
|
457
|
+
});
|
|
458
|
+
expect(await find({ orderBy, after: [b.id], where: { group: 1 } })).toEqual([]);
|
|
459
|
+
expect(await find({
|
|
460
|
+
orderBy,
|
|
461
|
+
after: [b.id],
|
|
462
|
+
where: { group: 1 },
|
|
463
|
+
withDeleted: true
|
|
464
|
+
})).toEqual([d.id]);
|
|
465
|
+
expect(await provider.count(context, {
|
|
466
|
+
orderBy,
|
|
467
|
+
after: [b.id]
|
|
468
|
+
})).toBe(5);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
374
471
|
const withTransactions = async () => {
|
|
375
472
|
const enabled = options.transactions ?? (await makeProvider()).capabilities?.transactions;
|
|
376
473
|
return enabled === true;
|
|
@@ -13,6 +13,11 @@ export interface ProviderContractOptions {
|
|
|
13
13
|
* Run the transaction cases. Defaults to `provider.capabilities?.transactions`.
|
|
14
14
|
*/
|
|
15
15
|
readonly transactions?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Run the cases of `after`, which `findCursor` gives to `findMany`. Defaults
|
|
18
|
+
* to `provider.capabilities?.keyset`.
|
|
19
|
+
*/
|
|
20
|
+
readonly keyset?: boolean;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Registers the behavior every `IRepositoryProvider` v2 must have. Pass the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afora/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Domain modeling on TypeBox: entities with invariants, repositories, use cases, sensitive data masking and layered exceptions, usable by any Standard Schema consumer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"domain-driven-design",
|