@marcelsud/keel.ts 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Or with Bun:
16
16
  bun add @marcelsud/keel.ts
17
17
  ```
18
18
 
19
- You need a running Keel environment. For the standalone storage example below, install the [Keel CLI](https://github.com/marcelsud/keel#install-the-cli) and run `keel` in another terminal first. To run a service with `keel dev`, configure its startup command in `keel.json`, for example `{ "command": ["bun", "run", "main.ts"] }`.
19
+ You need a running Keel environment. For the standalone storage example below, install the [Keel CLI](https://github.com/marcelsud/keel#install-the-cli) and run `keel` in another terminal first. To run a service with `keel dev`, configure its startup command in `keel.json`, for example `{ "command": ["bun", "run", "service.ts"] }`. The [first-service walkthrough](https://github.com/marcelsud/keel#start-a-service) describes the new `keel init` flow and its CLI version requirements; protobuf is optional for JSON actions, storage, events, and jobs.
20
20
 
21
21
  ## Connect and store data
22
22
 
@@ -37,6 +37,31 @@ try {
37
37
  }
38
38
  ```
39
39
 
40
+ ## Validate an action
41
+
42
+ The schema helpers require SDK 0.2.0 or later. Before `app.start()`, declare an action on your connected app:
43
+
44
+ ```ts
45
+ const createNote = app.action({
46
+ name: "createNote",
47
+ args: app.schema.object({
48
+ body: app.schema.string(),
49
+ labels: app.schema.string().array().optional(),
50
+ }),
51
+ async handler(ctx, input) {
52
+ const id = await ctx.collection.insert("notes", input);
53
+ return { id, body: input.body };
54
+ },
55
+ });
56
+
57
+ // After app.start():
58
+ const note = await createNote({ body: "Hello" });
59
+ ```
60
+
61
+ The SDK infers the handler input and action result. Missing or invalid fields return a 400 service error before the handler runs. Objects validate declared fields and strip extra fields; strings are checked without coercion or trimming.
62
+
63
+ Use `string()`, `number()`, `integer()`, `boolean()`, `object()`, `array()`, `record()`, `enum()`, `literal()`, `union()`, `null()`, and `unknown()`, with `.optional()` and `.nullable()` to compose shapes. You can also import `schema` and `type Infer` directly from the package, or continue supplying your own `args.parse()` parser. These helpers validate JSON actions; protobuf contracts remain the way to share generated API types across languages.
64
+
40
65
  ## Call a typed service
41
66
 
42
67
  Install the [Keel CLI](https://github.com/marcelsud/keel#install-the-cli). Its release bundle includes the generators; TypeScript generation uses your Node.js runtime.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { type NatsConnection } from "@nats-io/transport-node";
2
2
  import { ServiceError } from "@nats-io/services";
3
3
  import type { DescService } from "@bufbuild/protobuf";
4
+ import { schema, ValidationError } from "./schema.js";
4
5
  import { type Contract, type ContractOptions, type Schema, type ServiceClient, type ServiceImplementation } from "./protobuf.js";
5
- export { ServiceError };
6
+ export { ServiceError, schema, ValidationError };
7
+ export type { Infer, Validator } from "./schema.js";
6
8
  export type { Contract, ContractOptions, ContractState, Schema, ServiceClient, ServiceImplementation } from "./protobuf.js";
7
- export declare class ValidationError extends Error {
8
- }
9
9
  export interface Options {
10
10
  url?: string;
11
11
  name: string;
@@ -49,6 +49,24 @@ export interface HandlerContext {
49
49
  export declare class Keel implements HandlerContext {
50
50
  private nc;
51
51
  private options;
52
+ readonly schema: {
53
+ string: () => import("./schema.js").Validator<string, false>;
54
+ boolean: () => import("./schema.js").Validator<boolean, false>;
55
+ number: () => import("./schema.js").Validator<number, false>;
56
+ integer: () => import("./schema.js").Validator<number, false>;
57
+ null: () => import("./schema.js").Validator<null, false>;
58
+ unknown: () => import("./schema.js").Validator<unknown, false>;
59
+ any: () => import("./schema.js").Validator<unknown, false>;
60
+ object<S extends {
61
+ [x: string]: import("./schema.js").Validator<unknown, boolean>;
62
+ }>(shape: S): import("./schema.js").Validator<{ [K_1 in Exclude<keyof S, { [K in keyof S]: S[K]["isOptional"] extends true ? K : never; }[keyof S]>]: import("./schema.js").Infer<S[K_1]>; } & { [K_2 in { [K in keyof S]: S[K]["isOptional"] extends true ? K : never; }[keyof S]]?: import("./schema.js").Infer<S[K_2]> | undefined; }, false>;
63
+ array<T>(item: import("./schema.js").Validator<T>): import("./schema.js").Validator<T[], false>;
64
+ optional<T>(item: import("./schema.js").Validator<T>): import("./schema.js").Validator<T | undefined, true>;
65
+ record<T>(item: import("./schema.js").Validator<T>): import("./schema.js").Validator<Record<string, T>, false>;
66
+ literal<const T extends string | number | boolean | null>(expected: T): import("./schema.js").Validator<T, false>;
67
+ enum<const Values extends readonly [string, ...string[]]>(values: Values): import("./schema.js").Validator<Values[number], false>;
68
+ union<const V extends readonly [import("./schema.js").Validator, ...import("./schema.js").Validator[]]>(...members: V): import("./schema.js").Validator<import("./schema.js").Infer<V[number]>, false>;
69
+ };
52
70
  readonly kv: KV;
53
71
  readonly collection: Collections;
54
72
  readonly pubsub: HandlerContext["pubsub"];
package/dist/index.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { connect } from "@nats-io/transport-node";
2
2
  import { ServiceError, Svcm } from "@nats-io/services";
3
3
  import { jetstream } from "@nats-io/jetstream";
4
+ import { schema, ValidationError } from "./schema.js";
4
5
  import { protobufClient, schemaSubject, serviceEndpoints, validateSchema, } from "./protobuf.js";
5
- export { ServiceError };
6
+ export { ServiceError, schema, ValidationError };
6
7
  const prefix = "keel.v1";
7
8
  const identifier = /^[a-zA-Z0-9_-]+$/;
8
- export class ValidationError extends Error {
9
- }
10
9
  function name(value) {
11
10
  if (!identifier.test(value))
12
11
  throw new ValidationError("invalid service or action name");
@@ -65,6 +64,7 @@ export class Collections {
65
64
  export class Keel {
66
65
  nc;
67
66
  options;
67
+ schema = schema;
68
68
  kv;
69
69
  collection;
70
70
  pubsub;
@@ -0,0 +1,36 @@
1
+ export declare class ValidationError extends Error {
2
+ }
3
+ export interface Validator<T = unknown, Optional extends boolean = boolean> {
4
+ readonly isOptional: Optional;
5
+ parse(value: unknown, path?: string): T;
6
+ array(): Validator<T[], false>;
7
+ nullable(): Validator<T | null, Optional>;
8
+ optional(): Validator<T | undefined, true>;
9
+ }
10
+ export type Infer<V extends Validator> = V extends Validator<infer T> ? T : never;
11
+ type Shape = Record<string, Validator>;
12
+ type OptionalKeys<S extends Shape> = {
13
+ [K in keyof S]: S[K]["isOptional"] extends true ? K : never;
14
+ }[keyof S];
15
+ type ObjectValue<S extends Shape> = {
16
+ [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
17
+ } & {
18
+ [K in OptionalKeys<S>]?: Infer<S[K]>;
19
+ };
20
+ export declare const schema: {
21
+ string: () => Validator<string, false>;
22
+ boolean: () => Validator<boolean, false>;
23
+ number: () => Validator<number, false>;
24
+ integer: () => Validator<number, false>;
25
+ null: () => Validator<null, false>;
26
+ unknown: () => Validator<unknown, false>;
27
+ any: () => Validator<unknown, false>;
28
+ object<S extends Shape>(shape: S): Validator<ObjectValue<S>, false>;
29
+ array<T>(item: Validator<T>): Validator<T[], false>;
30
+ optional<T>(item: Validator<T>): Validator<T | undefined, true>;
31
+ record<T>(item: Validator<T>): Validator<Record<string, T>, false>;
32
+ literal<const T extends string | number | boolean | null>(expected: T): Validator<T, false>;
33
+ enum<const Values extends readonly [string, ...string[]]>(values: Values): Validator<Values[number], false>;
34
+ union<const V extends readonly [Validator, ...Validator[]]>(...members: V): Validator<Infer<V[number]>, false>;
35
+ };
36
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,87 @@
1
+ export class ValidationError extends Error {
2
+ }
3
+ function validator(parse, isOptional) {
4
+ return {
5
+ isOptional,
6
+ parse: (value, path = "value") => parse(value, path),
7
+ array() {
8
+ return validator((value, path) => {
9
+ if (!Array.isArray(value))
10
+ throw new ValidationError(`${path} must be an array`);
11
+ return value.map((item, index) => parse(item, `${path}[${index}]`));
12
+ }, false);
13
+ },
14
+ nullable() {
15
+ return validator((value, path) => value === null ? null : parse(value, path), isOptional);
16
+ },
17
+ optional() {
18
+ return validator((value, path) => value === undefined ? undefined : parse(value, path), true);
19
+ },
20
+ };
21
+ }
22
+ function scalar(type, accepts) {
23
+ return validator((value, path) => {
24
+ if (!accepts(value))
25
+ throw new ValidationError(`${path} must be ${type}`);
26
+ return value;
27
+ }, false);
28
+ }
29
+ function object(value, path) {
30
+ if (typeof value !== "object" || value === null ||
31
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
32
+ throw new ValidationError(`${path} must be an object`);
33
+ }
34
+ return value;
35
+ }
36
+ const unknown = () => validator((value) => value, false);
37
+ export const schema = {
38
+ string: () => scalar("a string", (value) => typeof value === "string"),
39
+ boolean: () => scalar("a boolean", (value) => typeof value === "boolean"),
40
+ number: () => scalar("a finite number", (value) => typeof value === "number" && Number.isFinite(value)),
41
+ integer: () => scalar("a safe integer", (value) => typeof value === "number" && Number.isSafeInteger(value)),
42
+ null: () => scalar("null", (value) => value === null),
43
+ unknown,
44
+ any: unknown,
45
+ object(shape) {
46
+ return validator((value, path) => {
47
+ const input = object(value, path);
48
+ return Object.fromEntries(Object.entries(shape).flatMap(([key, field]) => {
49
+ if (!Object.hasOwn(input, key)) {
50
+ if (field.isOptional)
51
+ return [];
52
+ throw new ValidationError(`${path}.${key} is required`);
53
+ }
54
+ return [[key, field.parse(input[key], `${path}.${key}`)]];
55
+ }));
56
+ }, false);
57
+ },
58
+ array(item) {
59
+ return item.array();
60
+ },
61
+ optional(item) {
62
+ return item.optional();
63
+ },
64
+ record(item) {
65
+ return validator((value, path) => Object.fromEntries(Object.entries(object(value, path)).map(([key, entry]) => [key, item.parse(entry, `${path}.${key}`)])), false);
66
+ },
67
+ literal(expected) {
68
+ return scalar(JSON.stringify(expected), (value) => value === expected);
69
+ },
70
+ enum(values) {
71
+ return scalar(`one of ${values.join(", ")}`, (value) => typeof value === "string" && values.includes(value));
72
+ },
73
+ union(...members) {
74
+ return validator((value, path) => {
75
+ let firstError;
76
+ for (const member of members) {
77
+ try {
78
+ return member.parse(value, path);
79
+ }
80
+ catch (error) {
81
+ firstError ??= error;
82
+ }
83
+ }
84
+ throw firstError;
85
+ }, false);
86
+ },
87
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcelsud/keel.ts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript SDK for Keel services, typed clients, storage, events, and background jobs.",
5
5
  "license": "MIT",
6
6
  "type": "module",