tshex-cli 1.0.28 → 1.0.29

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/build/main.js CHANGED
@@ -1,159 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { program } from 'commander';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import readline from 'node:readline/promises';
6
- import { stdin as input, stdout as output } from 'node:process';
7
- function readPackageJson() {
8
- const filePath = path.join(import.meta.dirname, '..', 'package.json');
9
- const fileContents = fs.readFileSync(filePath, 'utf-8');
10
- return JSON.parse(fileContents);
11
- }
12
- function executeCreateProject(templatesDir, projectDir) {
13
- try {
14
- fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
15
- recursive: true,
16
- filter: (src) => src.endsWith('.gitkeep') === false
17
- });
18
- console.log('Project created successfully');
19
- }
20
- catch (err) {
21
- console.error(err);
22
- }
23
- }
24
- function executeCreateContext(templatesDir, contextDir) {
25
- try {
26
- fs.cpSync(path.join(templatesDir, 'ctx'), contextDir, {
27
- recursive: true,
28
- filter: (src) => src.endsWith('.gitkeep') === false
29
- });
30
- console.log('Context created successfully');
31
- }
32
- catch (err) {
33
- console.error(err);
34
- }
35
- }
36
- function executeCreateReactContext(templatesDir, contextDir) {
37
- try {
38
- fs.cpSync(path.join(templatesDir, 'ctx-react'), contextDir, {
39
- recursive: true,
40
- filter: (src) => src.endsWith('.gitkeep') === false
41
- });
42
- console.log('React context created successfully');
43
- }
44
- catch (err) {
45
- console.error(err);
46
- }
47
- }
48
- function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir, rootSourceDir = sourceDir) {
49
- const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
50
- if (fs.existsSync(destinationDir) === false) {
51
- fs.mkdirSync(destinationDir, { recursive: true });
52
- }
53
- for (const entry of entries) {
54
- const sourcePath = path.join(sourceDir, entry.name);
55
- const destinationPath = path.join(destinationDir, entry.name);
56
- if (entry.isDirectory()) {
57
- if (entry.name === 'shared' && sourceDir === rootSourceDir) {
58
- continue;
59
- }
60
- if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
61
- continue;
62
- }
63
- if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
64
- continue;
65
- }
66
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir);
67
- continue;
68
- }
69
- if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
70
- fs.writeFileSync(destinationPath, fileContents);
71
- }
72
- }
73
- }
74
- async function ensureTestsDirectory(testsRootDir) {
75
- if (fs.existsSync(testsRootDir)) {
76
- if (fs.statSync(testsRootDir).isDirectory() === false) {
77
- throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`);
78
- }
79
- return;
80
- }
81
- const rl = readline.createInterface({ input, output });
82
- try {
83
- const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `);
84
- if (answer.trim().toLowerCase() !== 'y') {
85
- return false;
86
- }
87
- }
88
- finally {
89
- rl.close();
90
- }
91
- fs.mkdirSync(testsRootDir, { recursive: true });
92
- return true;
93
- }
94
- async function main(program) {
95
- const templatesDir = path.join(import.meta.dirname, '..', 'templates');
96
- const options = program.opts();
97
- let targetDir = path.resolve(options.dir ?? process.cwd());
98
- if (Object.keys(options).length === 0) {
99
- program.help();
100
- }
101
- if (fs.existsSync(targetDir) === false) {
102
- fs.mkdirSync(targetDir, { recursive: true });
103
- }
104
- if (options.project !== undefined) {
105
- targetDir = path.join(targetDir, options.project);
106
- executeCreateProject(templatesDir, targetDir);
107
- }
108
- if (options.react === true && options.context === undefined) {
109
- program.error('Option --react requires --context <name>');
110
- }
111
- if (options.context !== undefined) {
112
- targetDir = path.join(targetDir, options.context);
113
- if (options.react === true) {
114
- executeCreateReactContext(templatesDir, targetDir);
115
- }
116
- else {
117
- executeCreateContext(templatesDir, targetDir);
118
- }
119
- }
120
- if (options.tests !== undefined) {
121
- const sourceDir = path.resolve(options.tests);
122
- const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts');
123
- if (fs.existsSync(sourceDir) === false) {
124
- program.error(`Tests source directory does not exist: ${sourceDir}`);
125
- }
126
- if (fs.statSync(sourceDir).isDirectory() === false) {
127
- program.error(`Tests source path is not a directory: ${sourceDir}`);
128
- }
129
- if (fs.existsSync(testsTemplateFile) === false) {
130
- program.error(`Tests template file does not exist: ${testsTemplateFile}`);
131
- }
132
- const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8');
133
- const testsRootDir = path.join(targetDir, 'tests');
134
- const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir);
135
- if (testsDirectoryCreated === false) {
136
- program.error('Tests directory creation cancelled');
137
- }
138
- const testsDir = path.join(testsRootDir, path.basename(sourceDir));
139
- if (testsDir === sourceDir) {
140
- program.error('Tests destination directory cannot be the same as the source directory');
141
- }
142
- executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir);
143
- console.log('Tests structure created successfully');
144
- }
145
- }
146
- const packageJson = readPackageJson();
147
- program
148
- .name('tshex')
149
- .version(packageJson.version)
150
- .option('-P, --project <name>', "creates a new project with it's shared directory")
151
- .option('-C, --context <name>', 'creates a new context')
152
- .option('-R, --react', 'creates a React context with --context')
153
- .option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
154
- .option('--dir <path>', 'sets the directory to create the new item')
155
- .parse(process.argv);
156
- void main(program).catch((err) => {
157
- const message = err instanceof Error ? err.message : String(err);
158
- program.error(message);
159
- });
2
+ import{program as l}from"commander";import r from"node:fs";import n from"node:path";import p from"node:readline/promises";import{stdin as m,stdout as x}from"node:process";function h(){let e=n.join(import.meta.dirname,"..","package.json"),s=r.readFileSync(e,"utf-8");return JSON.parse(s)}function g(e,s){try{r.cpSync(n.join(e,"lib"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("Project created successfully")}catch(t){console.error(t)}}function j(e,s){try{r.cpSync(n.join(e,"ctx"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("Context created successfully")}catch(t){console.error(t)}}function S(e,s){try{r.cpSync(n.join(e,"ctx-react"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("React context created successfully")}catch(t){console.error(t)}}function d(e,s,t,i,c=e){let u=r.readdirSync(e,{withFileTypes:!0});r.existsSync(s)===!1&&r.mkdirSync(s,{recursive:!0});for(let o of u){let a=n.join(e,o.name),f=n.join(s,o.name);if(o.isDirectory()){if(o.name==="shared"&&e===c||i!==void 0&&a.startsWith(i)||r.existsSync(f)&&r.statSync(f).isDirectory()===!1)continue;d(a,f,t,i,c);continue}o.isFile()&&o.name.endsWith(".ts")&&r.existsSync(f)===!1&&r.writeFileSync(f,t)}}async function w(e){if(r.existsSync(e)){if(r.statSync(e).isDirectory()===!1)throw new Error(`Tests path exists but is not a directory: ${e}`);return}let s=p.createInterface({input:m,output:x});try{if((await s.question(`Tests directory does not exist at ${e}. Create it? (y/N) `)).trim().toLowerCase()!=="y")return!1}finally{s.close()}return r.mkdirSync(e,{recursive:!0}),!0}async function T(e){let s=n.join(import.meta.dirname,"..","templates"),t=e.opts(),i=n.resolve(t.dir??process.cwd());if(Object.keys(t).length===0&&e.help(),r.existsSync(i)===!1&&r.mkdirSync(i,{recursive:!0}),t.project!==void 0&&(i=n.join(i,t.project),g(s,i)),t.react===!0&&t.context===void 0&&e.error("Option --react requires --context <name>"),t.context!==void 0&&(i=n.join(i,t.context),t.react===!0?S(s,i):j(s,i)),t.tests!==void 0){let c=n.resolve(t.tests),u=n.join(s,"tests","content.ts");r.existsSync(c)===!1&&e.error(`Tests source directory does not exist: ${c}`),r.statSync(c).isDirectory()===!1&&e.error(`Tests source path is not a directory: ${c}`),r.existsSync(u)===!1&&e.error(`Tests template file does not exist: ${u}`);let o=r.readFileSync(u,"utf-8"),a=n.join(i,"tests");await w(a)===!1&&e.error("Tests directory creation cancelled");let y=n.join(a,n.basename(c));y===c&&e.error("Tests destination directory cannot be the same as the source directory"),d(c,y,o,a),console.log("Tests structure created successfully")}}var v=h();l.name("tshex").version(v.version).option("-P, --project <name>","creates a new project with it's shared directory").option("-C, --context <name>","creates a new context").option("-R, --react","creates a React context with --context").option("-T, --tests <path>","creates a .ts tests structure from an existing directory").option("--dir <path>","sets the directory to create the new item").parse(process.argv);T(l).catch(e=>{let s=e instanceof Error?e.message:String(e);l.error(s)});
@@ -49,7 +49,7 @@ The base class provides `none()` as an explicit empty result and requires
49
49
  contracts such as `Filterable`, `Creatable`, and `Updatable`, plus the
50
50
  `DatasetManager` extension for set operations.
51
51
 
52
- #### First Implementation
52
+ #### Implementation
53
53
 
54
54
  In the following example we implement an in-memory manager and its driver.
55
55
 
@@ -92,6 +92,18 @@ export class MemoryUsersDriver extends DriverAdapter<MemoryUsersManager> {
92
92
  raw records. The application layer can use both without knowing whether the
93
93
  source is memory, SQL, or an HTTP-backed adapter.
94
94
 
95
+ Put all your complex data operations in `DataManager`. `Repository` should only handle the transformation of raw records into domain representations. For example, if you need to relate users to their posts, implement that in a manager:
96
+
97
+ ```ts
98
+ class ComplexUsersManager extends DataManager<EnrichedUserRecord> {
99
+ ...
100
+
101
+ public async findAllAndRelate(): Promise<Array<EnrichedUserRecord>> {
102
+ ...
103
+ }
104
+ }
105
+ ```
106
+
95
107
  #### Repository
96
108
 
97
109
  `Repository` is responsible for transforming raw records into domain-oriented
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -41,15 +41,14 @@
41
41
  "bin": {
42
42
  "tshex": "./build/main.js"
43
43
  },
44
+ "scripts": {
45
+ "build": "deno run --allow-all npm:esbuild --bundle --minify --platform=node --external:commander --format=esm --outfile=./build/main.js ./source/main.ts",
46
+ "test": "deno test --allow-all tests/"
47
+ },
44
48
  "dependencies": {
45
- "commander": "^12.1.0"
49
+ "commander": "^15.0.0"
46
50
  },
47
51
  "devDependencies": {
48
- "@fission-ai/openspec": "^1.6.0",
49
- "@types/node": "^22.5.4",
50
- "typescript": "^5.4.5"
51
- },
52
- "scripts": {
53
- "build": "tsc"
52
+ "@types/node": "^18"
54
53
  }
55
54
  }
package/source/main.ts CHANGED
@@ -9,12 +9,12 @@ import { program } from 'commander'
9
9
  import fs from 'node:fs'
10
10
  import path from 'node:path'
11
11
  import readline from 'node:readline/promises'
12
- import { stdin as input, stdout as output } from 'node:process'
12
+ import { stdin, stdout } from 'node:process'
13
13
 
14
14
  // FUNCTIONS
15
15
 
16
16
  function readPackageJson() {
17
- const filePath = path.join(import.meta.dirname, '..', 'package.json')
17
+ const filePath = path.join(import.meta.dirname!, '..', 'package.json')
18
18
  const fileContents = fs.readFileSync(filePath, 'utf-8')
19
19
  return JSON.parse(fileContents)
20
20
  }
@@ -77,19 +77,35 @@ function executeCreateTests(
77
77
  continue
78
78
  }
79
79
 
80
- if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
80
+ if (
81
+ ignoredSourceDir !== undefined &&
82
+ sourcePath.startsWith(ignoredSourceDir)
83
+ ) {
81
84
  continue
82
85
  }
83
86
 
84
- if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
87
+ if (
88
+ fs.existsSync(destinationPath) &&
89
+ fs.statSync(destinationPath).isDirectory() === false
90
+ ) {
85
91
  continue
86
92
  }
87
93
 
88
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir)
94
+ executeCreateTests(
95
+ sourcePath,
96
+ destinationPath,
97
+ fileContents,
98
+ ignoredSourceDir,
99
+ rootSourceDir
100
+ )
89
101
  continue
90
102
  }
91
103
 
92
- if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
104
+ if (
105
+ entry.isFile() &&
106
+ entry.name.endsWith('.ts') &&
107
+ fs.existsSync(destinationPath) === false
108
+ ) {
93
109
  fs.writeFileSync(destinationPath, fileContents)
94
110
  }
95
111
  }
@@ -104,10 +120,12 @@ async function ensureTestsDirectory(testsRootDir: string) {
104
120
  return
105
121
  }
106
122
 
107
- const rl = readline.createInterface({ input, output })
123
+ const rl = readline.createInterface({ input: stdin, output: stdout })
108
124
 
109
125
  try {
110
- const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `)
126
+ const answer = await rl.question(
127
+ `Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `
128
+ )
111
129
 
112
130
  if (answer.trim().toLowerCase() !== 'y') {
113
131
  return false
@@ -121,7 +139,7 @@ async function ensureTestsDirectory(testsRootDir: string) {
121
139
  }
122
140
 
123
141
  async function main(program: typeof import('commander').program) {
124
- const templatesDir = path.join(import.meta.dirname, '..', 'templates')
142
+ const templatesDir = path.join(import.meta.dirname!, '..', 'templates')
125
143
 
126
144
  const options = program.opts()
127
145
 
@@ -182,7 +200,9 @@ async function main(program: typeof import('commander').program) {
182
200
  const testsDir = path.join(testsRootDir, path.basename(sourceDir))
183
201
 
184
202
  if (testsDir === sourceDir) {
185
- program.error('Tests destination directory cannot be the same as the source directory')
203
+ program.error(
204
+ 'Tests destination directory cannot be the same as the source directory'
205
+ )
186
206
  }
187
207
 
188
208
  executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir)
@@ -198,7 +218,10 @@ program
198
218
  .option('-P, --project <name>', "creates a new project with it's shared directory")
199
219
  .option('-C, --context <name>', 'creates a new context')
200
220
  .option('-R, --react', 'creates a React context with --context')
201
- .option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
221
+ .option(
222
+ '-T, --tests <path>',
223
+ 'creates a .ts tests structure from an existing directory'
224
+ )
202
225
  .option('--dir <path>', 'sets the directory to create the new item')
203
226
  .parse(process.argv)
204
227
 
@@ -1,4 +1,5 @@
1
1
  // Ports are exports from the context root level
2
+ // you can delete this file and create your own ports file in the context root level
2
3
 
3
4
  export function example(): void {
4
5
  // ...
@@ -0,0 +1,56 @@
1
+ type Generic = Record<string, unknown>
2
+
3
+ export interface Listable {
4
+ all(): Generic | Promise<Generic[]>
5
+ }
6
+
7
+ /**
8
+ * @description Declares a filtering operation over plain source records.
9
+ */
10
+ export interface Filterable {
11
+ filter(selector: unknown): Generic | Promise<Generic[]>
12
+ }
13
+
14
+ /**
15
+ * @description Declares a sorting operation over plain source records.
16
+ */
17
+ export interface Sortable {
18
+ sort(selector: unknown): Generic | Promise<Generic[]>
19
+ }
20
+
21
+ /**
22
+ * @description Declares a creation operation for plain source records.
23
+ */
24
+ export interface Creatable {
25
+ create(data: unknown): unknown
26
+ }
27
+
28
+ /**
29
+ * @description Declares an update operation that selects source records and applies new plain data.
30
+ */
31
+ export interface Updatable {
32
+ update(selector: unknown, data: unknown): unknown
33
+ }
34
+
35
+ /**
36
+ * @description Declares a deletion operation over source records selected by plain criteria.
37
+ */
38
+ export interface Deletable {
39
+ delete(selector: unknown): unknown
40
+ }
41
+
42
+ /**
43
+ * @description Declares an aggregation operation over source records.
44
+ */
45
+ export interface Aggregatable {
46
+ aggregate(selector: unknown): unknown
47
+ }
48
+
49
+ /**
50
+ * @description Declares operations for selecting or preloading relationships from a data source.
51
+ */
52
+ export interface Relatable {
53
+ selectRelated(...args: unknown[]): unknown
54
+
55
+ prefetchRelated(...args: unknown[]): unknown
56
+ }
@@ -1,66 +1,9 @@
1
- /**
2
- * @description Declares a filtering operation over plain source records.
3
- */
4
- export interface Filterable<S = Record<string, unknown>> {
5
- filter(selector: S): Promise<Array<S>>
6
- }
7
-
8
- /**
9
- * @description Declares a sorting operation over plain source records.
10
- */
11
- export interface Sortable<S = Record<string, unknown>> {
12
- sort(selector: S): Promise<Array<S>>
13
- }
14
-
15
- /**
16
- * @description Declares a creation operation for plain source records.
17
- */
18
- export interface Creatable<D = Record<string, unknown>> {
19
- create(data: D): Promise<unknown>
20
- }
21
-
22
- /**
23
- * @description Declares an update operation that selects source records and applies new plain data.
24
- */
25
- export interface Updatable<S = Record<string, unknown>, D = Record<string, unknown>> {
26
- update(selector: S, data: D): Promise<unknown>
27
- }
28
-
29
- /**
30
- * @description Declares a deletion operation over source records selected by plain criteria.
31
- */
32
- export interface Deletable<S = Record<string, unknown>> {
33
- delete(selector: S): Promise<unknown>
34
- }
35
-
36
- /**
37
- * @description Declares an aggregation operation over source records.
38
- */
39
- export interface Aggregatable<S = Record<string, unknown>> {
40
- aggregate(selector: S): Promise<S>
41
- }
42
-
43
- /**
44
- * @description Declares operations for selecting or preloading relationships from a data source.
45
- */
46
- export interface Relatable {
47
- selectRelated(...args: unknown[]): unknown
48
-
49
- prefetchRelated(...args: unknown[]): unknown
50
- }
51
-
52
1
  /**
53
2
  * @description Operates on a data source using plain objects and arrays.
54
3
  * It exposes the raw data without transforming it.
55
4
  */
56
5
  export abstract class DataManager<T = Record<string, unknown>> {
57
6
  [property: string]: unknown
58
-
59
- public none(): Array<T> {
60
- return []
61
- }
62
-
63
- public abstract all(): Promise<Array<T>>
64
7
  } //:: class
65
8
 
66
9
  /**
@@ -1,29 +1,18 @@
1
1
  import { type DataManager } from './managers.js'
2
2
  import { type DriverAdapter } from './drivers.js'
3
3
 
4
+ type Generic = Record<string, unknown>
5
+
4
6
  /**
5
7
  * @description Acts as an intermediary between plain source data and domain objects.
6
8
  * It transforms records into domain representations and can translate them back when needed.
7
9
  */
8
- export abstract class Repository<
9
- DataShape extends Record<string, unknown> = Record<string, unknown>,
10
- EntityShape extends Record<string, unknown> = Record<string, unknown>
11
- > {
10
+ export abstract class Repository<RawDataShape = Generic, EntityShape = Generic> {
12
11
  [property: string]: unknown
13
12
 
14
- public constructor(public readonly driver: DriverAdapter<DataManager<DataShape>>) {}
15
-
16
- public async all(): Promise<Array<EntityShape>> {
17
- const connection = await this.driver.connect()
18
- const raw = await connection.all()
19
- const entities = this.transformList(raw)
20
- await this.driver.disconnect()
21
- return entities
22
- }
23
-
24
- protected transformList(data: Array<DataShape>): Array<EntityShape> {
25
- return data.map(this.transform)
26
- }
13
+ public constructor(
14
+ public readonly driver: DriverAdapter<DataManager<RawDataShape>>
15
+ ) {}
27
16
 
28
- protected abstract transform(data: DataShape): EntityShape
17
+ protected abstract transform(data: RawDataShape): EntityShape
29
18
  } //:: class
@@ -1,6 +1,3 @@
1
- import { type TimeZone } from '../../types/timezones'
2
- import { type Locale } from '../../types/locales'
3
-
4
1
  export const DEBUG = 10
5
2
 
6
3
  export const INFO = 20
@@ -22,20 +19,6 @@ export abstract class Logger {
22
19
 
23
20
  public level: number = 0
24
21
 
25
- public datetimeLocales: Locale[] = ['en-GB']
26
-
27
- public datetimeFormatOptions: Intl.DateTimeFormatOptions & { timeZone: TimeZone } = {
28
- timeZone: 'UTC',
29
- year: 'numeric',
30
- month: '2-digit',
31
- day: '2-digit',
32
- hour: '2-digit',
33
- minute: '2-digit',
34
- second: '2-digit',
35
- fractionalSecondDigits: 3,
36
- hourCycle: 'h23'
37
- }
38
-
39
22
  public abstract debug(data: unknown): void
40
23
 
41
24
  public abstract info(data: unknown): void
@@ -45,8 +28,4 @@ export abstract class Logger {
45
28
  public abstract error(data: unknown): void
46
29
 
47
30
  public abstract critical(data: unknown): void
48
-
49
- protected getCurrentDatetime(): string {
50
- return new Date().toLocaleString(this.datetimeLocales, this.datetimeFormatOptions)
51
- }
52
31
  } //:: class
@@ -12,6 +12,6 @@ export abstract class Entity {
12
12
  }
13
13
 
14
14
  public toString(): string {
15
- return String(this.constructor.name)
15
+ return this.constructor.name
16
16
  }
17
17
  } //:: class
@@ -1,13 +0,0 @@
1
- /**
2
- * @description Http request handler to process incoming requests and generate responses.
3
- */
4
- export interface HttpRequestHandler {
5
- handle(request: Request): Response | Promise<Response>
6
- }
7
-
8
- /**
9
- * @description An HTTP middleware that pipes requests through handlers.
10
- */
11
- export interface HttpMiddleware {
12
- process(request: Request, handler: HttpRequestHandler): Response | Promise<Response>
13
- }