@valon-technologies/gestalt 0.0.1-alpha.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.
package/src/schema.ts ADDED
@@ -0,0 +1,219 @@
1
+ export type CatalogType = "string" | "integer" | "number" | "boolean" | "object" | "array";
2
+
3
+ export interface Schema<T> {
4
+ readonly catalogType: CatalogType;
5
+ readonly description: string;
6
+ readonly required: boolean;
7
+ readonly defaultValue: T | undefined;
8
+ readonly fields?: Record<string, Schema<unknown>>;
9
+ readonly item?: Schema<unknown>;
10
+ parse(value: unknown, path?: string): T;
11
+ }
12
+
13
+ export type InferSchema<TSchema extends Schema<unknown>> = TSchema extends Schema<infer T>
14
+ ? T
15
+ : never;
16
+
17
+ export interface SchemaOptions<T> {
18
+ description?: string;
19
+ required?: boolean;
20
+ default?: T;
21
+ }
22
+
23
+ const integerPattern = /^[+-]?\d+$/;
24
+ const numberPattern = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
25
+
26
+ function withMeta<T>(
27
+ catalogType: CatalogType,
28
+ parser: (value: unknown, path: string) => T,
29
+ options?: SchemaOptions<T>,
30
+ ): Schema<T> {
31
+ const description = options?.description?.trim() ?? "";
32
+ const hasDefault = options ? Object.prototype.hasOwnProperty.call(options, "default") : false;
33
+ const defaultValue = hasDefault ? cloneValue(options?.default) : undefined;
34
+ const required = options?.required ?? !hasDefault;
35
+
36
+ return {
37
+ catalogType,
38
+ description,
39
+ required,
40
+ defaultValue,
41
+ parse(value: unknown, path = "$") {
42
+ if (value === undefined || value === null) {
43
+ if (hasDefault) {
44
+ return cloneValue(defaultValue) as T;
45
+ }
46
+ if (!required) {
47
+ return undefined as T;
48
+ }
49
+ throw new TypeError(`${path} is required`);
50
+ }
51
+ return parser(value, path);
52
+ },
53
+ };
54
+ }
55
+
56
+ export function string(options?: SchemaOptions<string>): Schema<string> {
57
+ return withMeta(
58
+ "string",
59
+ (value, path) => {
60
+ if (typeof value === "string") {
61
+ return value;
62
+ }
63
+ throw new TypeError(`${path} must be a string`);
64
+ },
65
+ options,
66
+ );
67
+ }
68
+
69
+ export function integer(options?: SchemaOptions<number>): Schema<number> {
70
+ return withMeta(
71
+ "integer",
72
+ (value, path) => {
73
+ if (typeof value === "number" && Number.isInteger(value)) {
74
+ return value;
75
+ }
76
+ if (typeof value === "string") {
77
+ const trimmed = value.trim();
78
+ if (integerPattern.test(trimmed)) {
79
+ return Number.parseInt(trimmed, 10);
80
+ }
81
+ }
82
+ throw new TypeError(`${path} must be an integer`);
83
+ },
84
+ options,
85
+ );
86
+ }
87
+
88
+ export function number(options?: SchemaOptions<number>): Schema<number> {
89
+ return withMeta(
90
+ "number",
91
+ (value, path) => {
92
+ if (typeof value === "number" && Number.isFinite(value)) {
93
+ return value;
94
+ }
95
+ if (typeof value === "string") {
96
+ const trimmed = value.trim();
97
+ if (numberPattern.test(trimmed)) {
98
+ const parsed = Number.parseFloat(trimmed);
99
+ if (Number.isFinite(parsed)) {
100
+ return parsed;
101
+ }
102
+ }
103
+ }
104
+ throw new TypeError(`${path} must be a number`);
105
+ },
106
+ options,
107
+ );
108
+ }
109
+
110
+ export function boolean(options?: SchemaOptions<boolean>): Schema<boolean> {
111
+ return withMeta(
112
+ "boolean",
113
+ (value, path) => {
114
+ if (typeof value === "boolean") {
115
+ return value;
116
+ }
117
+ if (typeof value === "string") {
118
+ const normalized = value.trim().toLowerCase();
119
+ if (normalized === "true" || normalized === "1") {
120
+ return true;
121
+ }
122
+ if (normalized === "false" || normalized === "0") {
123
+ return false;
124
+ }
125
+ }
126
+ throw new TypeError(`${path} must be a boolean`);
127
+ },
128
+ options,
129
+ );
130
+ }
131
+
132
+ export function array<T>(item: Schema<T>, options?: SchemaOptions<T[]>): Schema<T[]> {
133
+ const base = withMeta<T[]>(
134
+ "array",
135
+ (value, path) => {
136
+ if (!Array.isArray(value)) {
137
+ throw new TypeError(`${path} must be an array`);
138
+ }
139
+ return value.map((entry, index) => item.parse(entry, `${path}[${index}]`));
140
+ },
141
+ options,
142
+ );
143
+
144
+ return {
145
+ ...base,
146
+ item,
147
+ };
148
+ }
149
+
150
+ export function object<T extends Record<string, unknown>>(
151
+ fields: { [K in keyof T]: Schema<T[K]> },
152
+ options?: SchemaOptions<T>,
153
+ ): Schema<T> {
154
+ const base = withMeta<T>(
155
+ "object",
156
+ (value, path) => {
157
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
158
+ throw new TypeError(`${path} must be an object`);
159
+ }
160
+ const source = value as Record<string, unknown>;
161
+ const output: Record<string, unknown> = {};
162
+ for (const [key, field] of Object.entries(fields)) {
163
+ const parsed = field.parse(source[key], `${path}.${key}`);
164
+ if (parsed !== undefined) {
165
+ output[key] = parsed;
166
+ }
167
+ }
168
+ return output as T;
169
+ },
170
+ options,
171
+ );
172
+
173
+ return {
174
+ ...base,
175
+ fields: fields as Record<string, Schema<unknown>>,
176
+ };
177
+ }
178
+
179
+ export function optional<T>(schema: Schema<T>): Schema<T | undefined> {
180
+ const wrapped: Schema<T | undefined> = {
181
+ catalogType: schema.catalogType,
182
+ description: schema.description,
183
+ required: false,
184
+ defaultValue: schema.defaultValue,
185
+ parse(value, path = "$") {
186
+ if (value === undefined || value === null) {
187
+ if (schema.defaultValue !== undefined) {
188
+ return cloneValue(schema.defaultValue);
189
+ }
190
+ return undefined;
191
+ }
192
+ return schema.parse(value, path);
193
+ },
194
+ };
195
+ const fields = schema.fields;
196
+ const item = schema.item;
197
+ return {
198
+ ...wrapped,
199
+ ...(fields !== undefined ? { fields } : {}),
200
+ ...(item !== undefined ? { item } : {}),
201
+ };
202
+ }
203
+
204
+ export const s = {
205
+ string,
206
+ integer,
207
+ number,
208
+ boolean,
209
+ array,
210
+ object,
211
+ optional,
212
+ };
213
+
214
+ function cloneValue<T>(value: T): T {
215
+ if (value === undefined) {
216
+ return value;
217
+ }
218
+ return JSON.parse(JSON.stringify(value)) as T;
219
+ }
package/src/secrets.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
2
+ import type { MaybePromise } from "./api.ts";
3
+
4
+ export interface SecretsProviderOptions extends RuntimeProviderOptions {
5
+ getSecret: (name: string) => MaybePromise<string>;
6
+ }
7
+
8
+ export class SecretsProvider extends RuntimeProvider {
9
+ readonly kind = "secrets" as const;
10
+
11
+ private readonly getSecretHandler: SecretsProviderOptions["getSecret"];
12
+
13
+ constructor(options: SecretsProviderOptions) {
14
+ super(options);
15
+ this.getSecretHandler = options.getSecret;
16
+ }
17
+
18
+ async getSecret(name: string): Promise<string> {
19
+ return await this.getSecretHandler(name);
20
+ }
21
+ }
22
+
23
+ export function defineSecretsProvider(options: SecretsProviderOptions): SecretsProvider {
24
+ return new SecretsProvider(options);
25
+ }
26
+
27
+ export function isSecretsProvider(value: unknown): value is SecretsProvider {
28
+ return (
29
+ value instanceof SecretsProvider ||
30
+ (typeof value === "object" &&
31
+ value !== null &&
32
+ "kind" in value &&
33
+ (value as { kind?: unknown }).kind === "secrets" &&
34
+ "getSecret" in value)
35
+ );
36
+ }
package/src/target.ts ADDED
@@ -0,0 +1,192 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isAbsolute, normalize, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { slugName, type ProviderKind } from "./provider.ts";
6
+
7
+ export type ModuleTarget = {
8
+ modulePath: string;
9
+ exportName?: string;
10
+ };
11
+
12
+ export type ProviderTarget = ModuleTarget & {
13
+ kind: ProviderKind;
14
+ };
15
+
16
+ export type PackageConfig = {
17
+ name?: string;
18
+ providerTarget?: ProviderTarget;
19
+ };
20
+
21
+ type PackageProviderConfig =
22
+ | string
23
+ | {
24
+ kind?: string;
25
+ target?: string;
26
+ };
27
+
28
+ const EXTERNAL_PROVIDER_KIND_TOKENS = new Set<string>([
29
+ "plugin",
30
+ "integration",
31
+ "auth",
32
+ "cache",
33
+ "secrets",
34
+ "s3",
35
+ "telemetry",
36
+ ]);
37
+
38
+ export function parseModuleTarget(target: string, label = "gestalt provider target"): ModuleTarget {
39
+ const [modulePathRaw, exportNameRaw] = target.split("#", 2);
40
+ const modulePath = modulePathRaw?.trim() ?? "";
41
+ const exportName = exportNameRaw?.trim() || undefined;
42
+
43
+ if (!modulePath) {
44
+ throw new Error(`${label} must include a relative module path`);
45
+ }
46
+ if (!modulePath.startsWith("./") && !modulePath.startsWith("../")) {
47
+ throw new Error(`${label} module path must be relative`);
48
+ }
49
+ if (exportName !== undefined && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportName)) {
50
+ throw new Error(`${label} export must be a valid JavaScript identifier`);
51
+ }
52
+
53
+ const parsed: ModuleTarget = {
54
+ modulePath,
55
+ };
56
+ if (exportName !== undefined) {
57
+ parsed.exportName = exportName;
58
+ }
59
+ return parsed;
60
+ }
61
+
62
+ export function parseProviderTarget(target: string | PackageProviderConfig): ProviderTarget {
63
+ if (typeof target === "string") {
64
+ const prefixed = parseKindPrefixedTarget(target);
65
+ if (prefixed) {
66
+ return prefixed;
67
+ }
68
+ return {
69
+ kind: "integration",
70
+ ...parseModuleTarget(target, "gestalt.provider"),
71
+ };
72
+ }
73
+
74
+ const kind = parseProviderKind(target.kind ?? "integration");
75
+ if (!target.target || typeof target.target !== "string") {
76
+ throw new Error("gestalt.provider.target is required");
77
+ }
78
+ return {
79
+ kind,
80
+ ...parseModuleTarget(target.target, "gestalt.provider.target"),
81
+ };
82
+ }
83
+
84
+ export const parsePluginTarget = parseModuleTarget;
85
+
86
+ export function readPackageConfig(root: string): PackageConfig {
87
+ const packagePath = resolve(root, "package.json");
88
+ const raw = JSON.parse(readFileSync(packagePath, "utf8")) as Record<string, unknown>;
89
+ const gestalt = (raw.gestalt ?? {}) as Record<string, unknown>;
90
+ const name = typeof raw.name === "string" ? raw.name.trim() : undefined;
91
+
92
+ let providerTarget: ProviderTarget | undefined;
93
+ if (typeof gestalt.provider === "string") {
94
+ providerTarget = parseProviderTarget(gestalt.provider);
95
+ } else if (isProviderConfigObject(gestalt.provider)) {
96
+ providerTarget = parseProviderTarget(gestalt.provider);
97
+ } else if (typeof gestalt.plugin === "string") {
98
+ providerTarget = {
99
+ kind: "integration",
100
+ ...parseModuleTarget(gestalt.plugin, "gestalt.plugin"),
101
+ };
102
+ }
103
+
104
+ const config: PackageConfig = {};
105
+ if (name !== undefined) {
106
+ config.name = name;
107
+ }
108
+ if (providerTarget !== undefined) {
109
+ config.providerTarget = providerTarget;
110
+ }
111
+ return config;
112
+ }
113
+
114
+ export function readPackageProviderTarget(root: string): ProviderTarget {
115
+ const config = readPackageConfig(root);
116
+ if (!config.providerTarget) {
117
+ throw new Error("package.json gestalt.provider or gestalt.plugin is required");
118
+ }
119
+ return config.providerTarget;
120
+ }
121
+
122
+ export function readPackagePluginTarget(root: string): string {
123
+ const target = readPackageProviderTarget(root);
124
+ if (target.kind !== "integration") {
125
+ throw new Error(`package.json provider kind ${JSON.stringify(target.kind)} is not an integration provider`);
126
+ }
127
+ return formatModuleTarget(target);
128
+ }
129
+
130
+ export function defaultProviderName(root: string): string {
131
+ const config = readPackageConfig(root);
132
+ return slugName(config.name ?? "");
133
+ }
134
+
135
+ export const defaultPluginName = defaultProviderName;
136
+
137
+ export function resolveProviderModulePath(root: string, target: ProviderTarget | ModuleTarget): string {
138
+ const absolute = resolve(root, target.modulePath);
139
+ if (!isAbsolute(absolute)) {
140
+ throw new Error("provider module path did not resolve to an absolute path");
141
+ }
142
+ return normalize(absolute);
143
+ }
144
+
145
+ export const resolvePluginModulePath = resolveProviderModulePath;
146
+
147
+ export function resolveProviderImportUrl(root: string, target: ProviderTarget | ModuleTarget): string {
148
+ return pathToFileURL(resolveProviderModulePath(root, target)).href;
149
+ }
150
+
151
+ export const resolvePluginImportUrl = resolveProviderImportUrl;
152
+
153
+ export function formatProviderTarget(target: ProviderTarget): string {
154
+ return `${formatProviderKind(target.kind)}:${formatModuleTarget(target)}`;
155
+ }
156
+
157
+ export function formatModuleTarget(target: ModuleTarget): string {
158
+ return `${target.modulePath}${target.exportName ? `#${target.exportName}` : ""}`;
159
+ }
160
+
161
+ function parseKindPrefixedTarget(target: string): ProviderTarget | undefined {
162
+ const match = target.match(/^(plugin|integration|auth|cache|secrets|s3|telemetry):(.*)$/);
163
+ if (!match) {
164
+ return undefined;
165
+ }
166
+ return {
167
+ kind: parseProviderKind(match[1]!),
168
+ ...parseModuleTarget(match[2]!, "provider target"),
169
+ };
170
+ }
171
+
172
+ function parseProviderKind(value: string): ProviderKind {
173
+ const normalized = value.trim().toLowerCase();
174
+ if (!EXTERNAL_PROVIDER_KIND_TOKENS.has(normalized)) {
175
+ throw new Error(`unsupported provider kind ${JSON.stringify(value)}`);
176
+ }
177
+ if (normalized === "plugin") {
178
+ return "integration";
179
+ }
180
+ return normalized as ProviderKind;
181
+ }
182
+
183
+ function formatProviderKind(kind: ProviderKind): string {
184
+ if (kind === "integration") {
185
+ return "plugin";
186
+ }
187
+ return kind;
188
+ }
189
+
190
+ function isProviderConfigObject(value: unknown): value is { kind?: string; target?: string } {
191
+ return typeof value === "object" && value !== null && !Array.isArray(value);
192
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Preserve",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true,
9
+ "verbatimModuleSyntax": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "bun"
15
+ ]
16
+ },
17
+ "include": [
18
+ "src/**/*.ts",
19
+ "tests/**/*.ts",
20
+ "gen/**/*.ts"
21
+ ]
22
+ }