@tstdl/base 0.91.52 → 0.92.0

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 (80) hide show
  1. package/ai/data-extracting.d.ts +35 -0
  2. package/ai/data-extracting.js +195 -41
  3. package/file/mime-type.d.ts +1 -1
  4. package/file/mime-type.js +8 -6
  5. package/file/mime-types.js +5 -32
  6. package/orm/schemas/numeric-date.d.ts +1 -1
  7. package/orm/schemas/timestamp.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/schema/converters/index.d.ts +1 -0
  10. package/schema/converters/index.js +1 -0
  11. package/schema/converters/openapi-converter.d.ts +3 -0
  12. package/schema/converters/openapi-converter.js +113 -0
  13. package/schema/decorators/description.d.ts +3 -0
  14. package/schema/decorators/description.js +10 -0
  15. package/schema/decorators/property.d.ts +3 -1
  16. package/schema/decorators/property.js +13 -5
  17. package/schema/decorators/types.d.ts +5 -5
  18. package/schema/schema.d.ts +7 -0
  19. package/schema/schema.js +6 -0
  20. package/schema/schemas/any.d.ts +4 -3
  21. package/schema/schemas/any.js +4 -4
  22. package/schema/schemas/array.d.ts +10 -5
  23. package/schema/schemas/array.js +7 -3
  24. package/schema/schemas/bigint.d.ts +6 -6
  25. package/schema/schemas/bigint.js +30 -13
  26. package/schema/schemas/boolean.d.ts +1 -1
  27. package/schema/schemas/boolean.js +2 -2
  28. package/schema/schemas/date.d.ts +1 -1
  29. package/schema/schemas/date.js +2 -2
  30. package/schema/schemas/defaulted.d.ts +5 -4
  31. package/schema/schemas/defaulted.js +6 -6
  32. package/schema/schemas/deferred.d.ts +1 -1
  33. package/schema/schemas/deferred.js +2 -2
  34. package/schema/schemas/enumeration.d.ts +5 -4
  35. package/schema/schemas/enumeration.js +4 -2
  36. package/schema/schemas/function.d.ts +3 -3
  37. package/schema/schemas/function.js +5 -4
  38. package/schema/schemas/instance.d.ts +5 -4
  39. package/schema/schemas/instance.js +6 -6
  40. package/schema/schemas/literal.d.ts +5 -4
  41. package/schema/schemas/literal.js +6 -6
  42. package/schema/schemas/never.d.ts +3 -2
  43. package/schema/schemas/never.js +2 -2
  44. package/schema/schemas/nullable.d.ts +1 -1
  45. package/schema/schemas/nullable.js +1 -1
  46. package/schema/schemas/number.d.ts +4 -2
  47. package/schema/schemas/number.js +6 -3
  48. package/schema/schemas/object.d.ts +2 -2
  49. package/schema/schemas/object.js +12 -7
  50. package/schema/schemas/one-or-many.d.ts +5 -4
  51. package/schema/schemas/one-or-many.js +6 -6
  52. package/schema/schemas/optional.d.ts +1 -1
  53. package/schema/schemas/optional.js +1 -1
  54. package/schema/schemas/readable-stream.d.ts +1 -1
  55. package/schema/schemas/readable-stream.js +2 -2
  56. package/schema/schemas/regexp.d.ts +1 -1
  57. package/schema/schemas/regexp.js +2 -2
  58. package/schema/schemas/simple.d.ts +3 -3
  59. package/schema/schemas/simple.js +1 -1
  60. package/schema/schemas/string.d.ts +1 -1
  61. package/schema/schemas/string.js +2 -2
  62. package/schema/schemas/symbol.d.ts +6 -6
  63. package/schema/schemas/symbol.js +8 -13
  64. package/schema/schemas/uint8-array.d.ts +1 -1
  65. package/schema/schemas/uint8-array.js +2 -2
  66. package/schema/schemas/union.js +3 -3
  67. package/schema/schemas/unknown.d.ts +4 -3
  68. package/schema/schemas/unknown.js +4 -4
  69. package/search-index/memory/memory-search-index.js +1 -1
  70. package/types.d.ts +1 -1
  71. package/utils/helpers.d.ts +0 -40
  72. package/utils/helpers.js +0 -29
  73. package/utils/index.d.ts +1 -0
  74. package/utils/index.js +1 -0
  75. package/utils/string/index.d.ts +1 -0
  76. package/utils/string/index.js +1 -0
  77. package/utils/string/normalize.d.ts +50 -0
  78. package/utils/string/normalize.js +39 -0
  79. package/utils/try-ignore.d.ts +2 -0
  80. package/utils/try-ignore.js +12 -0
@@ -1 +1,36 @@
1
1
  import '../polyfills.js';
2
+ import { GoogleGenerativeAI } from '@google/generative-ai';
3
+ import { type FileMetadataResponse, GoogleAIFileManager } from '@google/generative-ai/server';
4
+ import { Resolvable, type resolveArgumentType } from '../injector/interfaces.js';
5
+ import { SchemaTestable } from '../schema/index.js';
6
+ import { Enumeration as EnumerationType, EnumerationValue } from '../types.js';
7
+ import { LiteralUnion } from 'type-fest';
8
+ export type FileInput = {
9
+ path: string;
10
+ mimeType: string;
11
+ } | Blob;
12
+ export type GenerativeAIModel = LiteralUnion<'gemini-2.0-flash-exp' | 'gemini-exp-1206' | 'gemini-2.0-flash-thinking-exp-1219', string>;
13
+ export type AiServiceOptions = {
14
+ apiKey: string;
15
+ model?: GenerativeAIModel;
16
+ };
17
+ export type AiServiceArgument = AiServiceOptions;
18
+ export declare class AiService implements Resolvable<AiServiceArgument> {
19
+ #private;
20
+ readonly genAI: GoogleGenerativeAI;
21
+ readonly fileManager: GoogleAIFileManager;
22
+ readonly model: import("@google/generative-ai").GenerativeModel;
23
+ readonly [resolveArgumentType]: AiServiceArgument;
24
+ getFile(fileInput: FileInput): Promise<FileMetadataResponse>;
25
+ getFiles(files: FileInput[]): Promise<FileMetadataResponse[]>;
26
+ classify<T extends EnumerationType>(fileInput: FileInput, types: T): Promise<{
27
+ reasoning: string;
28
+ types: {
29
+ type: EnumerationValue<T>;
30
+ confidence: 'high' | 'medium' | 'low';
31
+ }[] | null;
32
+ }>;
33
+ extractData<T>(fileInput: FileInput, schema: SchemaTestable<T>): Promise<T>;
34
+ waitForFileActive(fileMetadata: FileMetadataResponse): Promise<FileMetadataResponse>;
35
+ waitForFilesActive(...files: FileMetadataResponse[]): Promise<FileMetadataResponse[]>;
36
+ }
@@ -4,59 +4,213 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
8
+ if (value !== null && value !== void 0) {
9
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
10
+ var dispose, inner;
11
+ if (async) {
12
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
13
+ dispose = value[Symbol.asyncDispose];
14
+ }
15
+ if (dispose === void 0) {
16
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
17
+ dispose = value[Symbol.dispose];
18
+ if (async) inner = dispose;
19
+ }
20
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
21
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
22
+ env.stack.push({ value: value, dispose: dispose, async: async });
23
+ }
24
+ else if (async) {
25
+ env.stack.push({ async: true });
26
+ }
27
+ return value;
28
+ };
29
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
30
+ return function (env) {
31
+ function fail(e) {
32
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
33
+ env.hasError = true;
34
+ }
35
+ var r, s = 0;
36
+ function next() {
37
+ while (r = env.stack.pop()) {
38
+ try {
39
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
40
+ if (r.dispose) {
41
+ var result = r.dispose.call(r.value);
42
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
43
+ }
44
+ else s |= 1;
45
+ }
46
+ catch (e) {
47
+ fail(e);
48
+ }
49
+ }
50
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
51
+ if (env.hasError) throw env.error;
52
+ }
53
+ return next();
54
+ };
55
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
56
+ var e = new Error(message);
57
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
58
+ });
7
59
  import '../polyfills.js';
60
+ import { openAsBlob } from 'node:fs';
61
+ import { unlink, writeFile } from 'node:fs/promises';
62
+ import { tmpdir } from 'node:os';
63
+ import { join } from 'node:path';
8
64
  import { GoogleGenerativeAI } from '@google/generative-ai';
9
65
  import { FileState, GoogleAIFileManager } from '@google/generative-ai/server';
10
- import { Application } from '../application/application.js';
11
66
  import { DetailsError } from '../errors/details.error.js';
12
67
  import { Singleton } from '../injector/decorators.js';
13
- import { inject } from '../injector/inject.js';
68
+ import { injectArgument } from '../injector/inject.js';
69
+ import { convertToOpenApiSchema } from '../schema/converters/openapi-converter.js';
70
+ import { array, enumeration, nullable, object, Schema, string } from '../schema/index.js';
71
+ import { digest } from '../utils/cryptography.js';
14
72
  import { timeout } from '../utils/timing.js';
15
- const generationConfig = {
16
- temperature: 1,
17
- topP: 0.95,
18
- topK: 40,
19
- maxOutputTokens: 8192,
20
- responseMimeType: 'text/plain',
21
- };
73
+ import { tryIgnoreAsync } from '../utils/try-ignore.js';
74
+ import { isBlob } from '../utils/type-guards.js';
75
+ import { millisecondsPerSecond } from '../utils/units.js';
22
76
  let AiService = class AiService {
23
- apiKey = 'AIzaSyDAFOSnn3it7EUIWNTp86kPb1os8P3nyu0';
24
- genAI = new GoogleGenerativeAI(this.apiKey);
25
- fileManager = new GoogleAIFileManager(this.apiKey);
26
- model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash-thinking-exp-1219' });
27
- async classify(_path) {
28
- }
29
- async extractData(path) {
30
- const { file } = await this.fileManager.uploadFile(path, { mimeType: 'application/pdf' });
31
- await this.waitForFilesActive(file);
32
- const chatSession = this.model.startChat({ generationConfig });
33
- const result = await chatSession.sendMessageStream([
34
- { fileData: { mimeType: file.mimeType, fileUri: file.uri } },
35
- { text: 'extract data as json' }
36
- ]);
37
- for await (const part of result.stream) {
38
- process.stdout.write(part.text());
39
- }
40
- console.log((await result.response).usageMetadata);
41
- }
42
- async waitForFilesActive(...files) {
43
- for (const name of files.map((file) => file.name)) {
44
- let file = await this.fileManager.getFile(name);
45
- while (file.state == FileState.PROCESSING) {
46
- await timeout(2500);
47
- file = await this.fileManager.getFile(name);
77
+ #options = injectArgument(this);
78
+ #fileCache = new Map();
79
+ genAI = new GoogleGenerativeAI(this.#options.apiKey);
80
+ fileManager = new GoogleAIFileManager(this.#options.apiKey);
81
+ model = this.genAI.getGenerativeModel({ model: this.#options.model ?? 'gemini-2.0-flash-exp' });
82
+ async getFile(fileInput) {
83
+ const path = isBlob(fileInput) ? join(tmpdir(), crypto.randomUUID()) : fileInput.path;
84
+ const mimeType = isBlob(fileInput) ? fileInput.type : fileInput.mimeType;
85
+ const blob = isBlob(fileInput) ? fileInput : await openAsBlob(path, { type: mimeType });
86
+ const buffer = await blob.arrayBuffer();
87
+ const byteArray = new Uint8Array(buffer);
88
+ const fileHash = await digest('SHA-256', byteArray).toBase64();
89
+ const fileKey = `${fileHash}:${byteArray.length}`;
90
+ if (this.#fileCache.has(fileKey)) {
91
+ try {
92
+ const cachedFile = await this.#fileCache.get(fileKey);
93
+ return await this.fileManager.getFile(cachedFile.name);
48
94
  }
49
- if (file.state == FileState.FAILED) {
50
- throw new DetailsError(file.error?.message ?? `Failed to process file ${file.name}`, file.error?.details);
95
+ catch {
96
+ this.#fileCache.delete(fileKey);
51
97
  }
52
98
  }
99
+ const filePromise = (async () => {
100
+ try {
101
+ const env_1 = { stack: [], error: void 0, hasError: false };
102
+ try {
103
+ const stack = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
104
+ if (isBlob(fileInput)) {
105
+ stack.defer(async () => tryIgnoreAsync(async () => unlink(path)));
106
+ await writeFile(path, byteArray);
107
+ }
108
+ const result = await this.fileManager.uploadFile(path, { mimeType });
109
+ return await this.waitForFileActive(result.file);
110
+ }
111
+ catch (e_1) {
112
+ env_1.error = e_1;
113
+ env_1.hasError = true;
114
+ }
115
+ finally {
116
+ const result_1 = __disposeResources(env_1);
117
+ if (result_1)
118
+ await result_1;
119
+ }
120
+ }
121
+ catch (error) {
122
+ this.#fileCache.delete(fileKey);
123
+ throw error;
124
+ }
125
+ })();
126
+ this.#fileCache.set(fileKey, filePromise);
127
+ return filePromise;
128
+ }
129
+ async getFiles(files) {
130
+ return Promise.all(files.map(async (file) => this.getFile(file)));
131
+ }
132
+ async classify(fileInput, types) {
133
+ const file = await this.getFile(fileInput);
134
+ const resultSchema = object({
135
+ reasoning: string({ description: 'Reasoning for classification. Use to be more confident, if unsure. Reason for every somewhat likely document type.' }),
136
+ types: nullable(array(object({
137
+ type: enumeration(types, { description: 'Type of document' }),
138
+ confidence: enumeration(['high', 'medium', 'low'], { description: 'How sure/certain you are about the classficiation.' })
139
+ }), { description: 'One or more document types that matches' }))
140
+ });
141
+ const responseSchema = convertToOpenApiSchema(resultSchema);
142
+ const result = await this.model.generateContent({
143
+ generationConfig: {
144
+ maxOutputTokens: 1024,
145
+ temperature: 0.5,
146
+ responseMimeType: 'application/json',
147
+ responseSchema
148
+ },
149
+ systemInstruction: 'You are a highly accurate document classification AI. Your task is to analyze the content of a given document and determine its type based on a predefined list of possible document types.',
150
+ contents: [
151
+ {
152
+ role: 'user',
153
+ parts: [
154
+ { fileData: { mimeType: file.mimeType, fileUri: file.uri } },
155
+ { text: `Classify the document. Output as JSON using the following schema:\n${JSON.stringify(responseSchema, null, 2)}\n\nIf none of the provided document types are a suitable match, return null for types.` }
156
+ ]
157
+ }
158
+ ]
159
+ });
160
+ return resultSchema.parse(JSON.parse(result.response.text()));
161
+ }
162
+ async extractData(fileInput, schema) {
163
+ const file = await this.getFile(fileInput);
164
+ const responseSchema = convertToOpenApiSchema(schema);
165
+ const result = await this.model.generateContent({
166
+ generationConfig: {
167
+ maxOutputTokens: 4096,
168
+ temperature: 0.5,
169
+ responseMimeType: 'application/json',
170
+ responseSchema
171
+ },
172
+ systemInstruction: `You are a highly skilled data extraction AI, specializing in accurately identifying and extracting information from unstructured text documents and converting it into a structured JSON format. Your primary goal is to meticulously follow the provided JSON schema and populate it with data extracted from the given document.
173
+
174
+ **Instructions:**
175
+ Carefully read and analyze the provided document. Identify relevant information that corresponds to each field in the JSON schema. Focus on accuracy and avoid making assumptions. If a field has multiple possible values, extract all relevant ones into the correct array structures ONLY IF the schema defines that field as an array; otherwise, extract only the single most relevant value.
176
+
177
+ **Reasoning**
178
+ Reason about every field in the json schema and find the best matching value. If there are multiple relevant values but the data type is not an array, reason about the values to find out which is the most relevant one.
179
+
180
+ You *MUST* output the reasoning first.`,
181
+ contents: [
182
+ {
183
+ role: 'user',
184
+ parts: [
185
+ { fileData: { mimeType: file.mimeType, fileUri: file.uri } },
186
+ { text: `Classify the document. Output as JSON using the following schema:\n${JSON.stringify(responseSchema, null, 2)}` }
187
+ ]
188
+ }
189
+ ]
190
+ });
191
+ return Schema.parse(schema, JSON.parse(result.response.text()));
192
+ }
193
+ async waitForFileActive(fileMetadata) {
194
+ let file = await this.fileManager.getFile(fileMetadata.name);
195
+ while (file.state == FileState.PROCESSING) {
196
+ await timeout(millisecondsPerSecond);
197
+ file = await this.fileManager.getFile(fileMetadata.name);
198
+ }
199
+ if (file.state == FileState.FAILED) {
200
+ throw new DetailsError(file.error?.message ?? `Failed to process file ${file.name}`, file.error?.details);
201
+ }
202
+ return file;
203
+ }
204
+ async waitForFilesActive(...files) {
205
+ const responses = [];
206
+ for (const file of files) {
207
+ const respones = await this.waitForFileActive(file);
208
+ responses.push(respones);
209
+ }
210
+ return responses;
53
211
  }
54
212
  };
55
213
  AiService = __decorate([
56
214
  Singleton()
57
215
  ], AiService);
58
- async function main() {
59
- const aiService = inject(AiService);
60
- await aiService.extractData('/home/patrick/Downloads/358 417/orig/358 417.pdf');
61
- }
62
- Application.run(main);
216
+ export { AiService };
@@ -1,2 +1,2 @@
1
- export declare function getMimeType(file: Uint8Array | ReadableStream<Uint8Array>): Promise<string>;
1
+ export declare function getMimeType(file: string | Uint8Array | ReadableStream<Uint8Array>): Promise<string>;
2
2
  export declare function getMimeTypeExtensions(mimeType: string): string[];
package/file/mime-type.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { dynamicImport } from '../import.js';
2
2
  import { decodeTextStream } from '../utils/encoding.js';
3
3
  import { readTextStream } from '../utils/stream/stream-reader.js';
4
- import { isUint8Array } from '../utils/type-guards.js';
4
+ import { isReadableStream, isString, isUint8Array } from '../utils/type-guards.js';
5
5
  import { mimeTypesMap } from './mime-types.js';
6
6
  export async function getMimeType(file) {
7
- return spawnFileCommand(['--brief', '--mime-type', '-'], file);
7
+ const path = isString(file) ? file : '-';
8
+ const data = isString(file) ? undefined : file;
9
+ return spawnFileCommand(['--brief', '--mime-type', path], data);
8
10
  }
9
11
  export function getMimeTypeExtensions(mimeType) {
10
12
  return mimeTypesMap.get(mimeType) ?? [];
@@ -15,7 +17,10 @@ async function spawnFileCommand(args, file) {
15
17
  const process = spawn('file', args, { stdio: 'pipe' });
16
18
  const stdin = Writable.toWeb(process.stdin);
17
19
  const stdout = Readable.toWeb(process.stdout).pipeThrough(decodeTextStream());
18
- if (isUint8Array(file)) {
20
+ if (isReadableStream(file)) {
21
+ await file.pipeTo(stdin);
22
+ }
23
+ else if (isUint8Array(file)) {
19
24
  const writer = stdin.getWriter();
20
25
  try {
21
26
  await writer.write(file);
@@ -23,9 +28,6 @@ async function spawnFileCommand(args, file) {
23
28
  }
24
29
  catch { /* File command closes stream as soon as it has the required data */ }
25
30
  }
26
- else {
27
- await file.pipeTo(stdin);
28
- }
29
31
  const output = await readTextStream(stdout);
30
32
  return output.trim();
31
33
  }
@@ -51,14 +51,7 @@ export const mimeTypes = {
51
51
  'application/mp4': ['mp4s'],
52
52
  'application/msword': ['doc', 'dot'],
53
53
  'application/mxf': ['mxf'],
54
- 'application/octet-stream': [
55
- 'bin', 'dms',
56
- 'lrf', 'mar',
57
- 'so', 'dist',
58
- 'distz', 'pkg',
59
- 'bpk', 'dump',
60
- 'elc', 'deploy'
61
- ],
54
+ 'application/octet-stream': ['bin', 'dms', 'lrf', 'mar', 'so', 'dist', 'distz', 'pkg', 'bpk', 'dump', 'elc', 'deploy'],
62
55
  'application/oda': ['oda'],
63
56
  'application/oebps-package+xml': ['opf'],
64
57
  'application/ogg': ['ogx'],
@@ -499,13 +492,7 @@ export const mimeTypes = {
499
492
  'application/x-csh': ['csh'],
500
493
  'application/x-debian-package': ['deb', 'udeb'],
501
494
  'application/x-dgc-compressed': ['dgc'],
502
- 'application/x-director': [
503
- 'dir', 'dcr',
504
- 'dxr', 'cst',
505
- 'cct', 'cxt',
506
- 'w3d', 'fgd',
507
- 'swa'
508
- ],
495
+ 'application/x-director': ['dir', 'dcr', 'dxr', 'cst', 'cct', 'cxt', 'w3d', 'fgd', 'swa'],
509
496
  'application/x-doom': ['wad'],
510
497
  'application/x-dtbncx+xml': ['ncx'],
511
498
  'application/x-dtbook+xml': ['dtb'],
@@ -583,11 +570,7 @@ export const mimeTypes = {
583
570
  'application/x-xliff+xml': ['xlf'],
584
571
  'application/x-xpinstall': ['xpi'],
585
572
  'application/x-xz': ['xz'],
586
- 'application/x-zmachine': [
587
- 'z1', 'z2', 'z3',
588
- 'z4', 'z5', 'z6',
589
- 'z7', 'z8'
590
- ],
573
+ 'application/x-zmachine': ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'],
591
574
  'application/xaml+xml': ['xaml'],
592
575
  'application/xcap-diff+xml': ['xdf'],
593
576
  'application/xenc+xml': ['xenc'],
@@ -710,12 +693,7 @@ export const mimeTypes = {
710
693
  'text/html': ['html', 'htm'],
711
694
  'text/javascript': ['js', 'mjs'],
712
695
  'text/n3': ['n3'],
713
- 'text/plain': [
714
- 'txt', 'text',
715
- 'conf', 'def',
716
- 'list', 'log',
717
- 'in'
718
- ],
696
+ 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in'],
719
697
  'text/prs.lines.tag': ['dsc'],
720
698
  'text/richtext': ['rtx'],
721
699
  'text/sgml': ['sgml', 'sgm'],
@@ -738,12 +716,7 @@ export const mimeTypes = {
738
716
  'text/vnd.wap.wml': ['wml'],
739
717
  'text/vnd.wap.wmlscript': ['wmls'],
740
718
  'text/x-asm': ['s', 'asm'],
741
- 'text/x-c': [
742
- 'c', 'cc',
743
- 'cxx', 'cpp',
744
- 'h', 'hh',
745
- 'dic'
746
- ],
719
+ 'text/x-c': ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'],
747
720
  'text/x-fortran': ['f', 'for', 'f77', 'f90'],
748
721
  'text/x-java-source': ['java'],
749
722
  'text/x-nfo': ['nfo'],
@@ -1,5 +1,5 @@
1
1
  import { NumberSchema, type NumberSchemaOptions, type SchemaPropertyDecorator, type SchemaPropertyDecoratorOptions, type SimpleSchemaOptions } from '../../schema/index.js';
2
- export type NumericDateSchemaOptions = SimpleSchemaOptions & Pick<NumberSchemaOptions, 'minimum' | 'maximum'>;
2
+ export type NumericDateSchemaOptions = SimpleSchemaOptions<number> & Pick<NumberSchemaOptions, 'minimum' | 'maximum'>;
3
3
  export declare class NumericDateSchema extends NumberSchema {
4
4
  readonly name = "NumericDate";
5
5
  constructor(options?: NumericDateSchemaOptions);
@@ -1,6 +1,6 @@
1
1
  import type { JsonPath } from '../../json-path/json-path.js';
2
2
  import { NumberSchema, type NumberSchemaOptions, type SchemaPropertyDecorator, type SchemaPropertyDecoratorOptions, type SchemaTestOptions, type SchemaTestResult, type SimpleSchemaOptions } from '../../schema/index.js';
3
- export type TimestampSchemaOptions = SimpleSchemaOptions & Pick<NumberSchemaOptions, 'minimum' | 'maximum'>;
3
+ export type TimestampSchemaOptions = SimpleSchemaOptions<number> & Pick<NumberSchemaOptions, 'minimum' | 'maximum'>;
4
4
  export declare class TimestampSchema extends NumberSchema {
5
5
  readonly name = "Timestamp";
6
6
  constructor(options?: TimestampSchemaOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.91.52",
3
+ "version": "0.92.0",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1 @@
1
+ export * from './openapi-converter.js';
@@ -0,0 +1 @@
1
+ export * from './openapi-converter.js';
@@ -0,0 +1,3 @@
1
+ import type { UndefinableJsonObject } from '../../types.js';
2
+ import type { SchemaTestable } from '../schema.js';
3
+ export declare function convertToOpenApiSchema(testable: SchemaTestable): UndefinableJsonObject;
@@ -0,0 +1,113 @@
1
+ import { NotSupportedError } from '../../errors/not-supported.error.js';
2
+ import { fromEntries, objectEntries } from '../../utils/object/object.js';
3
+ import { isDefined, isNotNull, isNumber, isString } from '../../utils/type-guards.js';
4
+ import { ArraySchema } from '../schemas/array.js';
5
+ import { BooleanSchema } from '../schemas/boolean.js';
6
+ import { DateSchema } from '../schemas/date.js';
7
+ import { EnumerationSchema } from '../schemas/enumeration.js';
8
+ import { LiteralSchema } from '../schemas/literal.js';
9
+ import { nullable, NullableSchema } from '../schemas/nullable.js';
10
+ import { NumberSchema } from '../schemas/number.js';
11
+ import { ObjectSchema } from '../schemas/object.js';
12
+ import { OptionalSchema } from '../schemas/optional.js';
13
+ import { StringSchema } from '../schemas/string.js';
14
+ import { UnionSchema } from '../schemas/union.js';
15
+ import { schemaTestableToSchema } from '../testable.js';
16
+ export function convertToOpenApiSchema(testable) {
17
+ const schema = schemaTestableToSchema(testable);
18
+ return {
19
+ ...convertToOpenApiSchemaBase(schema),
20
+ ...(isNotNull(schema.description) ? { description: schema.description } : undefined),
21
+ ...(isDefined(schema.example) ? { example: schema.example } : undefined)
22
+ };
23
+ }
24
+ function convertToOpenApiSchemaBase(schema) {
25
+ if (schema instanceof ObjectSchema) {
26
+ const entries = objectEntries(schema.properties);
27
+ const convertedEntries = entries.map(([property, propertySchema]) => [property, convertToOpenApiSchema(stripOptional(propertySchema))]);
28
+ return {
29
+ type: 'object',
30
+ properties: fromEntries(convertedEntries),
31
+ required: entries
32
+ .filter(([, propertySchema]) => !(propertySchema instanceof OptionalSchema) && !((propertySchema instanceof NullableSchema) && (propertySchema.schema instanceof OptionalSchema)))
33
+ .map(([property]) => property)
34
+ };
35
+ }
36
+ if (schema instanceof StringSchema) {
37
+ return {
38
+ type: 'string'
39
+ };
40
+ }
41
+ if (schema instanceof DateSchema) {
42
+ return {
43
+ type: 'string',
44
+ format: 'date-time'
45
+ };
46
+ }
47
+ if (schema instanceof NumberSchema) {
48
+ return {
49
+ type: schema.integer ? 'integer' : 'number',
50
+ ...(isNumber(schema.minimum) ? { minimum: schema.minimum } : undefined),
51
+ ...(isNumber(schema.maximum) ? { maximum: schema.maximum } : undefined)
52
+ };
53
+ }
54
+ if (schema instanceof BooleanSchema) {
55
+ return {
56
+ type: 'boolean'
57
+ };
58
+ }
59
+ if (schema instanceof LiteralSchema) {
60
+ return {
61
+ type: typeof schema.value,
62
+ enum: [schema.value]
63
+ };
64
+ }
65
+ if (schema instanceof ArraySchema) {
66
+ return {
67
+ type: 'array',
68
+ items: convertToOpenApiSchema(schema.itemSchema),
69
+ ...(isNumber(schema.minimum) ? { minItems: schema.minimum } : undefined),
70
+ ...(isNumber(schema.maximum) ? { maxItems: schema.maximum } : undefined)
71
+ };
72
+ }
73
+ if (schema instanceof EnumerationSchema) {
74
+ const hasString = schema.allowedValues.some(isString);
75
+ const hasNumber = schema.allowedValues.some(isNumber);
76
+ if (hasString && hasNumber) {
77
+ throw new NotSupportedError('Enum must be either string or number but not both.');
78
+ }
79
+ return {
80
+ type: hasString ? 'string' : 'number',
81
+ enum: schema.allowedValues
82
+ };
83
+ }
84
+ if (schema instanceof NullableSchema) {
85
+ if (schema.schema instanceof EnumerationSchema) {
86
+ const enumSchema = convertToOpenApiSchema(schema.schema);
87
+ return {
88
+ ...enumSchema,
89
+ enum: [...enumSchema.enum, null],
90
+ nullable: true
91
+ };
92
+ }
93
+ return {
94
+ ...convertToOpenApiSchema(schema.schema),
95
+ nullable: true
96
+ };
97
+ }
98
+ if (schema instanceof UnionSchema) {
99
+ return {
100
+ oneOf: schema.schemas.map((innerSchema) => convertToOpenApiSchema(innerSchema))
101
+ };
102
+ }
103
+ throw new NotSupportedError(`Schema "${schema.name}" not supported.`);
104
+ }
105
+ function stripOptional(schema) {
106
+ if ((schema instanceof OptionalSchema)) {
107
+ return schema.schema;
108
+ }
109
+ if ((schema instanceof NullableSchema) && (schema.schema instanceof OptionalSchema)) {
110
+ return nullable(schema.schema.schema);
111
+ }
112
+ return schema;
113
+ }
@@ -0,0 +1,3 @@
1
+ import { type Decorator } from '../../reflection/index.js';
2
+ import type { SchemaTypeReflectionData } from './types.js';
3
+ export declare function Description(options?: SchemaTypeReflectionData): Decorator<'class' | 'property'>;
@@ -0,0 +1,10 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import { createDecorator } from '../../reflection/index.js';
3
+ export function Description(options = {}) {
4
+ return createDecorator({
5
+ class: true,
6
+ property: true,
7
+ data: { schema: options },
8
+ mergeData: true
9
+ });
10
+ }
@@ -2,9 +2,11 @@ import type { SetRequired } from 'type-fest';
2
2
  import type { Decorator } from '../../reflection/index.js';
3
3
  import type { TypedOmit } from '../../types.js';
4
4
  import type { SchemaTestable } from '../schema.js';
5
- import type { SchemaPropertyReflectionData } from './types.js';
5
+ import type { SchemaPropertyReflectionData, SchemaTestableProvider } from './types.js';
6
6
  export type SchemaPropertyDecoratorOptions = SchemaPropertyReflectionData;
7
7
  export type SchemaPropertyDecoratorOptionsWithRequiredSchema = SetRequired<SchemaPropertyReflectionData, 'schema'>;
8
8
  export type SchemaPropertyDecoratorOptionsWithoutSchema = TypedOmit<SchemaPropertyReflectionData, 'schema'>;
9
9
  export declare function Property(schema: SchemaTestable, options?: SchemaPropertyDecoratorOptionsWithoutSchema): Decorator<'property' | 'accessor'>;
10
10
  export declare function Property(options: SchemaPropertyDecoratorOptionsWithRequiredSchema): Decorator<'property' | 'accessor'>;
11
+ export declare function PropertySchema(schemaProvider: SchemaTestableProvider, options?: SchemaPropertyDecoratorOptionsWithoutSchema): Decorator<'property' | 'accessor'>;
12
+ export declare function PropertySchema(options: SchemaPropertyDecoratorOptionsWithRequiredSchema): Decorator<'property' | 'accessor'>;
@@ -1,13 +1,21 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
- import { isDefined } from '../../utils/type-guards.js';
3
- import { isSchemaTestable } from '../testable.js';
2
+ import { isDefined, isFunction } from '../../utils/type-guards.js';
4
3
  import { createSchemaPropertyDecorator } from './utils.js';
5
4
  export function Property(schemaOrOptions, optionsOrNothing) {
6
5
  if (isDefined(optionsOrNothing)) {
7
- return createSchemaPropertyDecorator({ ...optionsOrNothing, schema: schemaOrOptions });
6
+ return createSchemaPropertyDecorator({ ...optionsOrNothing, schema: () => schemaOrOptions });
8
7
  }
9
- if (isSchemaTestable(schemaOrOptions)) {
10
- return createSchemaPropertyDecorator({ schema: schemaOrOptions });
8
+ if (isFunction(schemaOrOptions)) {
9
+ return createSchemaPropertyDecorator({ schema: () => schemaOrOptions });
11
10
  }
12
11
  return createSchemaPropertyDecorator(schemaOrOptions);
13
12
  }
13
+ export function PropertySchema(schemaProviderOrOptions, optionsOrNothing) {
14
+ if (isDefined(optionsOrNothing)) {
15
+ return createSchemaPropertyDecorator({ ...optionsOrNothing, schema: schemaProviderOrOptions });
16
+ }
17
+ if (isFunction(schemaProviderOrOptions)) {
18
+ return createSchemaPropertyDecorator({ schema: schemaProviderOrOptions });
19
+ }
20
+ return createSchemaPropertyDecorator(schemaProviderOrOptions);
21
+ }
@@ -1,16 +1,16 @@
1
1
  import type { Decorator } from '../../reflection/types.js';
2
- import type { SchemaTestable } from '../schema.js';
2
+ import type { SchemaOptions, SchemaTestable } from '../schema.js';
3
3
  import type { ObjectSchemaFactory, ObjectSchemaOptions } from '../schemas/object.js';
4
4
  export type SchemaClassDecorator = Decorator<'class'>;
5
5
  export type SchemaPropertyDecorator = Decorator<'property' | 'accessor'>;
6
- export type SchemaTypeReflectionData = Partial<Pick<ObjectSchemaOptions, 'mask' | 'unknownProperties' | 'unknownPropertiesKey'>> & {
6
+ export type SchemaTypeReflectionData = Partial<Pick<ObjectSchemaOptions, 'mask' | 'unknownProperties' | 'unknownPropertiesKey' | 'description' | 'example'>> & {
7
7
  schema?: SchemaTestable;
8
8
  factory?: ObjectSchemaFactory<any>;
9
9
  };
10
- export type SchemaPropertyReflectionData = {
11
- schema?: SchemaTestable;
10
+ export type SchemaTestableProvider = (data: SchemaPropertyReflectionData) => SchemaTestable;
11
+ export type SchemaPropertyReflectionData = Partial<Pick<SchemaOptions<any>, 'description' | 'example'>> & {
12
+ schema?: SchemaTestableProvider;
12
13
  array?: boolean;
13
14
  optional?: boolean;
14
15
  nullable?: boolean;
15
- data?: Record<PropertyKey, any>;
16
16
  };