@kensio/yulin 0.0.1

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.
Files changed (34) hide show
  1. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  2. package/.idea/jsLinters/eslint.xml +6 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/prettier.xml +7 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/.idea/yulin.iml +8 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc +4 -0
  9. package/LICENSE +661 -0
  10. package/README.md +11 -0
  11. package/eslint.config.ts +208 -0
  12. package/package.json +69 -0
  13. package/pnpm-workspace.yaml +1 -0
  14. package/src/command/command-handler.ts +3 -0
  15. package/src/service/dynamodb/command/create-table/create-table.handler.ts +59 -0
  16. package/src/service/dynamodb/command/create-table/create-table.iso.test.ts +67 -0
  17. package/src/service/dynamodb/command/describe-table/describe-table.handler.ts +50 -0
  18. package/src/service/dynamodb/command/describe-table/describe-table.iso.test.ts +67 -0
  19. package/src/service/dynamodb/command/list-tables/list-tables.handler.ts +51 -0
  20. package/src/service/dynamodb/command/list-tables/list-tables.iso.test.ts +74 -0
  21. package/src/service/dynamodb/dynamodb-table.iso.test.ts +41 -0
  22. package/src/service/dynamodb/dynamodb-table.ts +46 -0
  23. package/src/service/dynamodb/dynamodb.ts +46 -0
  24. package/src/service/organizations/sim-aws-account.ts +36 -0
  25. package/src/util/background/background.iso.test.ts +74 -0
  26. package/src/util/background/background.ts +62 -0
  27. package/src/util/brand.type.ts +11 -0
  28. package/src/util/memo/memo.iso.test.ts +43 -0
  29. package/src/util/memo/memo.ts +26 -0
  30. package/src/util/sleep.ts +16 -0
  31. package/tsconfig.build.json +13 -0
  32. package/tsconfig.json +36 -0
  33. package/typings/eslint-plugin-promise.d.ts +11 -0
  34. package/vitest.config.ts +50 -0
@@ -0,0 +1,41 @@
1
+ import { describe, it } from "vitest";
2
+ import { CreateTableCommand } from "@aws-sdk/client-dynamodb";
3
+ import { SimDynamoDbTable } from "./dynamodb-table.js";
4
+ import {
5
+ assertIdentical,
6
+ assertInstanceOf,
7
+ assertThrowsError,
8
+ } from "@kensio/smartass";
9
+
10
+ describe("SimDynamoDbTable", () => {
11
+ it("throws when TableName is undefined", () => {
12
+ const command = new CreateTableCommand({ TableName: undefined });
13
+
14
+ assertThrowsError(() => new SimDynamoDbTable(command));
15
+ });
16
+
17
+ it("creates table with CREATING status", () => {
18
+ const command = new CreateTableCommand({
19
+ TableName: "test-table",
20
+ });
21
+
22
+ const table = new SimDynamoDbTable(command);
23
+
24
+ assertIdentical(table.tableName, "test-table");
25
+ assertIdentical(table.status, "CREATING");
26
+ assertInstanceOf(table.creationDateTime, Date);
27
+ });
28
+
29
+ it("activates table", async () => {
30
+ const command = new CreateTableCommand({
31
+ TableName: "test-table",
32
+ });
33
+
34
+ const table = new SimDynamoDbTable(command);
35
+ assertIdentical(table.status, "CREATING");
36
+
37
+ await table.activate();
38
+
39
+ assertIdentical(table.status, "ACTIVE");
40
+ });
41
+ });
@@ -0,0 +1,46 @@
1
+ import type { Brand } from "../../util/brand.type.js";
2
+ import type { CreateTableCommand, TableStatus } from "@aws-sdk/client-dynamodb";
3
+
4
+ export type DynamoDbTableName = Brand<string, "DynamoDbTableName">;
5
+
6
+ /**
7
+ * Simulated DynamoDB Table.
8
+ */
9
+ export class SimDynamoDbTable {
10
+ public readonly creationDateTime: Date;
11
+
12
+ private readonly _tableName: DynamoDbTableName;
13
+ private _status: TableStatus = "CREATING";
14
+
15
+ constructor(private readonly createCommand: CreateTableCommand) {
16
+ if (createCommand.input.TableName === undefined) {
17
+ throw new Error(
18
+ "DynamoDB CreateTableCommand.input.TableName is required",
19
+ );
20
+ }
21
+ this._tableName = this.createCommand.input.TableName as DynamoDbTableName;
22
+ this.creationDateTime = new Date();
23
+ }
24
+
25
+ /**
26
+ * Simulate the table entering ACTIVE status.
27
+ */
28
+ activate(): Promise<void> {
29
+ this._status = "ACTIVE";
30
+ return Promise.resolve();
31
+ }
32
+
33
+ /**
34
+ * Get the DynamoDB table name.
35
+ */
36
+ public get tableName(): DynamoDbTableName {
37
+ return this._tableName;
38
+ }
39
+
40
+ /**
41
+ * Get the current table status.
42
+ */
43
+ public get status(): TableStatus {
44
+ return this._status;
45
+ }
46
+ }
@@ -0,0 +1,46 @@
1
+ import type {
2
+ CreateTableCommand,
3
+ CreateTableOutput,
4
+ DescribeTableCommand,
5
+ DescribeTableOutput,
6
+ ListTablesCommand,
7
+ ListTablesOutput,
8
+ } from "@aws-sdk/client-dynamodb";
9
+ import type { DynamoDbTableName, SimDynamoDbTable } from "./dynamodb-table.js";
10
+ import { CreateTableCommandHandler } from "./command/create-table/create-table.handler.js";
11
+ import type { BackgroundScheduler } from "../../util/background/background.js";
12
+ import { ListTablesCommandHandler } from "./command/list-tables/list-tables.handler.js";
13
+ import { DescribeTableCommandHandler } from "./command/describe-table/describe-table.handler.js";
14
+
15
+ /**
16
+ * Simulated DynamoDB. Handles SDK commands. Emulates AWS behaviour and state.
17
+ */
18
+ export class SimDynamoDb {
19
+ private readonly tables = new Map<DynamoDbTableName, SimDynamoDbTable>();
20
+
21
+ constructor(private readonly background: BackgroundScheduler) {}
22
+
23
+ /**
24
+ * Handle a Create Table Command from the SDK.
25
+ */
26
+ async createTable(cmd: CreateTableCommand): Promise<CreateTableOutput> {
27
+ const handler = new CreateTableCommandHandler(this.tables, this.background);
28
+ return await handler.handle(cmd);
29
+ }
30
+
31
+ /**
32
+ * Handle a List Tables Command from the SDK.
33
+ */
34
+ async listTables(cmd: ListTablesCommand): Promise<ListTablesOutput> {
35
+ const handler = new ListTablesCommandHandler(this.tables);
36
+ return await handler.handle(cmd);
37
+ }
38
+
39
+ /**
40
+ * Handle a Describe Table Command from the SDK.
41
+ */
42
+ async describeTable(cmd: DescribeTableCommand): Promise<DescribeTableOutput> {
43
+ const handler = new DescribeTableCommandHandler(this.tables);
44
+ return await handler.handle(cmd);
45
+ }
46
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ type BackgroundCompleter,
3
+ type BackgroundScheduler,
4
+ BackgroundTasks,
5
+ } from "../../util/background/background.js";
6
+ import { SimDynamoDb } from "../dynamodb/dynamodb.js";
7
+ import { Memo } from "../../util/memo/memo.js";
8
+
9
+ /**
10
+ * Container for simulated AWS services.
11
+ */
12
+ export class SimAwsAccount {
13
+ private readonly memo = new Memo<object>();
14
+
15
+ constructor(
16
+ private readonly background: BackgroundScheduler &
17
+ BackgroundCompleter = new BackgroundTasks(),
18
+ ) {}
19
+
20
+ /**
21
+ * Wait for all outstanding background tasks in this Account to complete.
22
+ */
23
+ async backgroundTasksComplete(): Promise<void> {
24
+ await this.background.complete();
25
+ }
26
+
27
+ /**
28
+ * Get the simulated DynamoDB service in this simulated Account.
29
+ */
30
+ getDynamoDb(): SimDynamoDb {
31
+ return this.memo.getOrCreate(
32
+ "DynamoDB",
33
+ () => new SimDynamoDb(this.background),
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it } from "vitest";
2
+ import { BackgroundTasks } from "./background.js";
3
+ import {
4
+ assertFalse,
5
+ assertIdentical,
6
+ assertThrowsErrorAsync,
7
+ assertTrue,
8
+ } from "@kensio/smartass";
9
+
10
+ describe("BackgroundTasks", () => {
11
+ it("executes scheduled tasks", async () => {
12
+ const tasks = new BackgroundTasks();
13
+ let executed = false;
14
+
15
+ tasks.schedule(async () => {
16
+ await Promise.resolve();
17
+ executed = true;
18
+ });
19
+
20
+ assertFalse(executed);
21
+ await tasks.complete();
22
+ assertTrue(executed);
23
+ });
24
+
25
+ it("tracks pending task count", async () => {
26
+ const tasks = new BackgroundTasks();
27
+
28
+ assertIdentical(tasks.size, 0);
29
+
30
+ tasks.schedule(async () => {
31
+ /* empty */
32
+ });
33
+ tasks.schedule(async () => {
34
+ /* empty */
35
+ });
36
+ tasks.schedule(async () => {
37
+ /* empty */
38
+ });
39
+
40
+ assertIdentical(tasks.size, 3);
41
+ await tasks.complete();
42
+ assertIdentical(tasks.size, 0);
43
+ });
44
+
45
+ it("handles tasks that schedule more tasks", async () => {
46
+ const tasks = new BackgroundTasks();
47
+ const execOrder: number[] = [];
48
+
49
+ tasks.schedule(async () => {
50
+ await Promise.resolve();
51
+ execOrder.push(1);
52
+ tasks.schedule(async () => {
53
+ await Promise.resolve();
54
+ execOrder.push(2);
55
+ });
56
+ });
57
+
58
+ await tasks.complete();
59
+ assertIdentical(execOrder.length, 2);
60
+ assertIdentical(execOrder[0], 1);
61
+ assertIdentical(execOrder[1], 2);
62
+ });
63
+
64
+ it("propagates task errors", async () => {
65
+ const tasks = new BackgroundTasks();
66
+
67
+ tasks.schedule(async () => {
68
+ await Promise.resolve();
69
+ throw new Error("Task failed");
70
+ });
71
+
72
+ await assertThrowsErrorAsync(async () => tasks.complete());
73
+ });
74
+ });
@@ -0,0 +1,62 @@
1
+ export type BackgroundTask = () => Promise<void>;
2
+
3
+ export interface BackgroundScheduler {
4
+ schedule(task: BackgroundTask): void;
5
+ }
6
+
7
+ export interface BackgroundCompleter {
8
+ complete(): Promise<void>;
9
+ }
10
+
11
+ /**
12
+ * Simple async background tasks scheduler, to simulate asynchronous distributed
13
+ * operations occurring in a non-deterministic sequence.
14
+ */
15
+ export class BackgroundTasks
16
+ implements BackgroundScheduler, BackgroundCompleter
17
+ {
18
+ private readonly pending = new Set<Promise<void>>();
19
+
20
+ /**
21
+ * Schedule a task to happen asynchronously in the background.
22
+ */
23
+ schedule(task: BackgroundTask): void {
24
+ const promise = new Promise<void>((resolve) => {
25
+ setTimeout(resolve, Math.random() * 3);
26
+ })
27
+ .then(task)
28
+ .then(
29
+ () => {
30
+ //
31
+ },
32
+ (error: unknown) => {
33
+ // Keep the promise rejected so drain() can surface failures
34
+ // deterministically.
35
+ throw error;
36
+ },
37
+ )
38
+ .finally(() => {
39
+ this.pending.delete(promise);
40
+ });
41
+
42
+ this.pending.add(promise);
43
+ }
44
+
45
+ /**
46
+ * Wait until all tasks currently scheduled have finished.
47
+ * If tasks schedule more tasks, this will continue draining until idle.
48
+ */
49
+ public async complete(): Promise<void> {
50
+ while (this.pending.size > 0) {
51
+ // eslint-disable-next-line no-await-in-loop
52
+ await Promise.all(this.pending);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * See how many outstanding background tasks are scheduled.
58
+ */
59
+ public get size(): number {
60
+ return this.pending.size;
61
+ }
62
+ }
@@ -0,0 +1,11 @@
1
+ export type Brand<K, T extends string> = K & Record<T, never>;
2
+
3
+ export type ISODateString = Brand<
4
+ `${number}-${number}-${number}T${number}:${number}:${number}.${number}Z`,
5
+ "ISODateString"
6
+ >;
7
+ declare global {
8
+ interface Date {
9
+ toISOString(): ISODateString;
10
+ }
11
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it } from "vitest";
2
+ import { Memo } from "./memo.js";
3
+ import { assertIdentical } from "@kensio/smartass";
4
+
5
+ describe("Memo", () => {
6
+ it("creates and caches values", () => {
7
+ const memo = new Memo<string>();
8
+ let callCount = 0;
9
+
10
+ const value1 = memo.getOrCreate("key1", () => {
11
+ callCount++;
12
+ return "value1";
13
+ });
14
+ const value2 = memo.getOrCreate("key1", () => {
15
+ callCount++;
16
+ return "different";
17
+ });
18
+
19
+ assertIdentical(value1, "value1");
20
+ assertIdentical(value2, "value1");
21
+ assertIdentical(callCount, 1);
22
+ });
23
+
24
+ it("handles multiple keys independently", () => {
25
+ const memo = new Memo<number>();
26
+
27
+ const a = memo.getOrCreate("a", () => 10);
28
+ const b = memo.getOrCreate("b", () => 20);
29
+ const c = memo.getOrCreate("a", () => 99);
30
+
31
+ assertIdentical(a, 10);
32
+ assertIdentical(b, 20);
33
+ assertIdentical(c, 10);
34
+ });
35
+
36
+ it("checks key existence", () => {
37
+ const memo = new Memo<string>();
38
+
39
+ assertIdentical(memo.has("key"), false);
40
+ memo.getOrCreate("key", () => "value");
41
+ assertIdentical(memo.has("key"), true);
42
+ });
43
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Simple memoization cache.
3
+ */
4
+ export class Memo<T> {
5
+ private readonly cache = new Map<PropertyKey, T>();
6
+
7
+ /**
8
+ * Returns the cached value for the given key, or creates/caches it using `factory`.
9
+ */
10
+ getOrCreate<V extends T>(key: PropertyKey, factory: () => V): V {
11
+ if (this.cache.has(key)) {
12
+ return this.cache.get(key) as V;
13
+ }
14
+
15
+ const value = factory();
16
+ this.cache.set(key, value);
17
+ return value;
18
+ }
19
+
20
+ /**
21
+ * Returns whether the cache contains `key`.
22
+ */
23
+ has(key: PropertyKey): boolean {
24
+ return this.cache.has(key);
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Async sleep for an approximate number of milliseconds.
3
+ */
4
+ export async function sleep(ms = 0): Promise<void> {
5
+ await new Promise((resolve) => {
6
+ setTimeout(resolve, ms);
7
+ });
8
+ }
9
+
10
+ /**
11
+ * Sleep for a random but short length of time, e.g. to simulate asynchronous
12
+ * operations completing in a non-deterministic order.
13
+ */
14
+ export async function jitter(): Promise<void> {
15
+ await sleep(Math.random() * 2);
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "noEmitOnError": true
10
+ },
11
+ "include": ["src/**/*", "typings/**/*.d.ts"],
12
+ "exclude": ["**/*.test.*", "**/*.spec.*", "test/**/*"]
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023"],
7
+ "types": ["node"],
8
+
9
+ "strict": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noImplicitOverride": true,
12
+ "noImplicitReturns": true,
13
+ "noPropertyAccessFromIndexSignature": true,
14
+ "noUncheckedIndexedAccess": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "allowUnusedLabels": false,
19
+ "allowUnreachableCode": false,
20
+
21
+ "isolatedModules": true,
22
+ "verbatimModuleSyntax": true,
23
+ "skipLibCheck": true,
24
+ "forceConsistentCasingInFileNames": true,
25
+ "resolveJsonModule": true,
26
+
27
+ "declaration": true,
28
+ "declarationMap": true,
29
+ "sourceMap": true,
30
+ "removeComments": false,
31
+
32
+ "outDir": "dist",
33
+ "rootDir": "."
34
+ },
35
+ "include": ["src/**/*", "test/**/*", "typings/**/*.d.ts"]
36
+ }
@@ -0,0 +1,11 @@
1
+ declare module "eslint-plugin-promise" {
2
+ import type { ESLint } from "eslint";
3
+
4
+ const plugin: ESLint.Plugin & {
5
+ configs: {
6
+ "flat/recommended": ESLint.Plugin;
7
+ };
8
+ };
9
+
10
+ export default plugin;
11
+ }
@@ -0,0 +1,50 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { configDefaults, defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "#test": fileURLToPath(new URL("./test", import.meta.url)),
8
+ },
9
+ },
10
+ test: {
11
+ environment: "node",
12
+ include: ["src/**/*.test.ts"],
13
+ typecheck: {
14
+ tsconfig: "./tsconfig.json",
15
+ },
16
+ coverage: {
17
+ provider: "v8",
18
+ include: ["src/**/*.ts"],
19
+ exclude: [...configDefaults.exclude],
20
+ reporter: ["text", "lcov"],
21
+ reportsDirectory: "./test/.coverage",
22
+ thresholds: {
23
+ statements: 90,
24
+ branches: 90,
25
+ functions: 90,
26
+ lines: 90,
27
+ },
28
+ },
29
+ restoreMocks: true,
30
+ testTimeout: 10_000,
31
+ projects: [
32
+ {
33
+ extends: true,
34
+ test: {
35
+ name: "isolatedTests",
36
+ include: ["src/**/*.iso.test.ts"],
37
+ // globalSetup: ["./test/isoTestGlobalSetUp.ts"],
38
+ },
39
+ },
40
+ {
41
+ extends: true,
42
+ test: {
43
+ name: "localTests",
44
+ include: ["src/**/*.loc.test.ts"],
45
+ // globalSetup: ["./test/locTestGlobalSetUp.ts"],
46
+ },
47
+ },
48
+ ],
49
+ },
50
+ });