upstash-lua 0.3.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 +319 -0
- package/dist/define-script.d.ts +250 -0
- package/dist/define-script.d.ts.map +1 -0
- package/dist/errors.d.ts +107 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/eval-with-cache.d.ts +73 -0
- package/dist/eval-with-cache.d.ts.map +1 -0
- package/dist/hash-result.d.ts +48 -0
- package/dist/hash-result.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/lua-template.d.ts +158 -0
- package/dist/lua-template.d.ts.map +1 -0
- package/dist/redis-like.d.ts +90 -0
- package/dist/redis-like.d.ts.map +1 -0
- package/dist/sha1.d.ts +28 -0
- package/dist/sha1.d.ts.map +1 -0
- package/dist/standard-schema.d.ts +112 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/version.d.ts +19 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
Type-safe Lua scripts for Upstash Redis with StandardSchemaV1 validation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Full TypeScript inference** for keys, args, and return values
|
|
8
|
+
- **Type-safe Lua templates** - Use `${KEYS.name}` and `${ARGV.name}` with autocomplete and compile-time errors
|
|
9
|
+
- **Input validation** using StandardSchemaV1 schemas (Zod, Effect Schema, ArkType, etc.)
|
|
10
|
+
- **Efficient execution** via EVALSHA with automatic NOSCRIPT fallback
|
|
11
|
+
- **Universal runtime support** - Node.js 18+, Bun, Cloudflare Workers, Vercel Edge
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add upstash-lua @upstash/redis zod
|
|
17
|
+
# or
|
|
18
|
+
npm install upstash-lua @upstash/redis zod
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { z } from "zod"
|
|
25
|
+
import { defineScript, lua } from "upstash-lua"
|
|
26
|
+
import { Redis } from "@upstash/redis"
|
|
27
|
+
|
|
28
|
+
const redis = new Redis({
|
|
29
|
+
url: process.env.UPSTASH_REDIS_REST_URL,
|
|
30
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Define a rate limiter script with type-safe Lua template
|
|
34
|
+
const rateLimit = defineScript({
|
|
35
|
+
name: "rateLimit",
|
|
36
|
+
keys: {
|
|
37
|
+
key: z.string(),
|
|
38
|
+
},
|
|
39
|
+
args: {
|
|
40
|
+
limit: z.number().int().positive().transform(String),
|
|
41
|
+
windowSeconds: z.number().int().positive().transform(String),
|
|
42
|
+
},
|
|
43
|
+
lua: ({ KEYS, ARGV }) => lua`
|
|
44
|
+
local current = redis.call("INCR", ${KEYS.key})
|
|
45
|
+
if current == 1 then
|
|
46
|
+
redis.call("EXPIRE", ${KEYS.key}, ${ARGV.windowSeconds})
|
|
47
|
+
end
|
|
48
|
+
local allowed = current <= tonumber(${ARGV.limit}) and 1 or 0
|
|
49
|
+
return { allowed, tonumber(${ARGV.limit}) - current }
|
|
50
|
+
`,
|
|
51
|
+
returns: z.tuple([z.number(), z.number()]).transform(([allowed, rem]) => ({
|
|
52
|
+
allowed: allowed === 1,
|
|
53
|
+
remaining: rem,
|
|
54
|
+
})),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Execute with full type safety
|
|
58
|
+
const result = await rateLimit.run(redis, {
|
|
59
|
+
keys: { key: "rl:user:123" },
|
|
60
|
+
args: { limit: 10, windowSeconds: 60 },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
console.log(result.allowed) // boolean
|
|
64
|
+
console.log(result.remaining) // number
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### `defineScript(options)`
|
|
70
|
+
|
|
71
|
+
Creates a type-safe Lua script definition.
|
|
72
|
+
|
|
73
|
+
#### Options
|
|
74
|
+
|
|
75
|
+
| Property | Type | Description |
|
|
76
|
+
|----------|------|-------------|
|
|
77
|
+
| `name` | `string` | Human-readable name (used in error messages) |
|
|
78
|
+
| `lua` | `string \| LuaFunction` | Lua script source code (string) or type-safe template function |
|
|
79
|
+
| `keys` | `Record<string, Schema>` | Key schemas - order determines KEYS[1], KEYS[2], etc. |
|
|
80
|
+
| `args` | `Record<string, Schema>` | Arg schemas - order determines ARGV[1], ARGV[2], etc. |
|
|
81
|
+
| `returns` | `Schema` | Optional return value schema |
|
|
82
|
+
|
|
83
|
+
**Important:** Key/arg order is determined by object literal insertion order. Always define using object literal syntax in the intended order.
|
|
84
|
+
|
|
85
|
+
#### Lua Property: String vs Function
|
|
86
|
+
|
|
87
|
+
The `lua` property can be either a plain string or a function that returns a `lua` template:
|
|
88
|
+
|
|
89
|
+
**String form** (manual KEYS/ARGV indexing):
|
|
90
|
+
```typescript
|
|
91
|
+
lua: `return redis.call("GET", KEYS[1])`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Function form** (type-safe with autocomplete):
|
|
95
|
+
```typescript
|
|
96
|
+
lua: ({ KEYS, ARGV }) => lua`
|
|
97
|
+
return redis.call("GET", ${KEYS.userKey})
|
|
98
|
+
`
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The function form provides:
|
|
102
|
+
- ✅ Autocomplete for `KEYS.*` and `ARGV.*` properties
|
|
103
|
+
- ✅ Compile-time errors for invalid key/arg references
|
|
104
|
+
- ✅ Automatic compilation of `${KEYS.name}` → `KEYS[n]` and `${ARGV.name}` → `ARGV[n]`
|
|
105
|
+
|
|
106
|
+
#### Returns
|
|
107
|
+
|
|
108
|
+
A `Script` object with:
|
|
109
|
+
|
|
110
|
+
- `run(redis, input)` - Execute with full validation
|
|
111
|
+
- `runRaw(redis, input)` - Execute without return validation
|
|
112
|
+
- `name`, `lua`, `keyNames`, `argNames` - Metadata
|
|
113
|
+
|
|
114
|
+
### Schema Requirements
|
|
115
|
+
|
|
116
|
+
Keys and args must output strings (Redis only accepts strings). Use transforms:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
args: {
|
|
120
|
+
// Number input → string output
|
|
121
|
+
limit: z.number().transform(String),
|
|
122
|
+
|
|
123
|
+
// Boolean input → "1" or "0" output
|
|
124
|
+
enabled: z.boolean().transform(b => b ? "1" : "0"),
|
|
125
|
+
|
|
126
|
+
// String input → string output (no transform needed)
|
|
127
|
+
key: z.string(),
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
### Type-Safe Lua Template
|
|
134
|
+
|
|
135
|
+
Use the `lua` tagged template function for type-safe key and argument references:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { defineScript, lua } from "upstash-lua"
|
|
139
|
+
import { z } from "zod"
|
|
140
|
+
|
|
141
|
+
const getUser = defineScript({
|
|
142
|
+
name: "getUser",
|
|
143
|
+
keys: {
|
|
144
|
+
userKey: z.string(),
|
|
145
|
+
},
|
|
146
|
+
args: {
|
|
147
|
+
field: z.string(),
|
|
148
|
+
},
|
|
149
|
+
lua: ({ KEYS, ARGV }) => lua`
|
|
150
|
+
-- TypeScript autocomplete works here!
|
|
151
|
+
local key = ${KEYS.userKey}
|
|
152
|
+
local field = ${ARGV.field}
|
|
153
|
+
return redis.call("HGET", key, field)
|
|
154
|
+
`,
|
|
155
|
+
returns: z.string().nullable(),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ${KEYS.userKey} automatically becomes KEYS[1]
|
|
159
|
+
// ${ARGV.field} automatically becomes ARGV[1]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Simple Script (No Keys/Args)
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const ping = defineScript({
|
|
166
|
+
name: "ping",
|
|
167
|
+
lua: 'return redis.call("PING")',
|
|
168
|
+
returns: z.string(),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const result = await ping.run(redis)
|
|
172
|
+
// result: "PONG"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or with the function form:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const ping = defineScript({
|
|
179
|
+
name: "ping",
|
|
180
|
+
lua: () => lua`return redis.call("PING")`,
|
|
181
|
+
returns: z.string(),
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Script Without Return Validation
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const getData = defineScript({
|
|
189
|
+
name: "getData",
|
|
190
|
+
lua: 'return redis.call("HGETALL", KEYS[1])',
|
|
191
|
+
keys: { key: z.string() },
|
|
192
|
+
// No returns schema - result is unknown
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const result = await getData.run(redis, { keys: { key: "user:123" } })
|
|
196
|
+
// result: unknown
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### HGETALL with `hashResult()`
|
|
200
|
+
|
|
201
|
+
Redis commands like `HGETALL` return flat arrays of alternating key-value pairs:
|
|
202
|
+
`["field1", "value1", "field2", "value2"]`
|
|
203
|
+
|
|
204
|
+
The `hashResult()` helper converts this to an object before validation, enabling you to use `z.object()`:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { z } from "zod"
|
|
208
|
+
import { defineScript, hashResult } from "upstash-lua"
|
|
209
|
+
|
|
210
|
+
const getUser = defineScript({
|
|
211
|
+
name: "getUser",
|
|
212
|
+
keys: { key: z.string() },
|
|
213
|
+
lua: 'return redis.call("HGETALL", KEYS[1])',
|
|
214
|
+
returns: hashResult(z.object({
|
|
215
|
+
name: z.string(),
|
|
216
|
+
email: z.string(),
|
|
217
|
+
age: z.coerce.number(),
|
|
218
|
+
is_admin: z.string().transform(v => v === "true"),
|
|
219
|
+
})),
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const user = await getUser.run(redis, { keys: { key: "user:123" } })
|
|
223
|
+
// user: { name: string, email: string, age: number, is_admin: boolean }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Works with all Zod object features:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Optional fields
|
|
230
|
+
returns: hashResult(z.object({
|
|
231
|
+
name: z.string(),
|
|
232
|
+
email: z.string().optional(),
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
// Partial objects
|
|
236
|
+
returns: hashResult(z.object({
|
|
237
|
+
name: z.string(),
|
|
238
|
+
email: z.string(),
|
|
239
|
+
}).partial())
|
|
240
|
+
|
|
241
|
+
// Passthrough for extra fields
|
|
242
|
+
returns: hashResult(z.object({
|
|
243
|
+
name: z.string(),
|
|
244
|
+
}).passthrough())
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Effect Schema Example
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { Schema } from "effect"
|
|
251
|
+
import { defineScript, lua } from "upstash-lua"
|
|
252
|
+
|
|
253
|
+
const incr = defineScript({
|
|
254
|
+
name: "incr",
|
|
255
|
+
keys: {
|
|
256
|
+
key: Schema.standardSchemaV1(Schema.String),
|
|
257
|
+
},
|
|
258
|
+
args: {
|
|
259
|
+
amount: Schema.standardSchemaV1(
|
|
260
|
+
Schema.Number.pipe(
|
|
261
|
+
Schema.transform(Schema.String, (n) => String(n), (s) => Number(s))
|
|
262
|
+
)
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
lua: ({ KEYS, ARGV }) => lua`
|
|
266
|
+
return redis.call("INCRBY", ${KEYS.key}, ${ARGV.amount})
|
|
267
|
+
`,
|
|
268
|
+
returns: Schema.standardSchemaV1(Schema.Number),
|
|
269
|
+
})
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Error Handling
|
|
273
|
+
|
|
274
|
+
Errors are thrown as `Error` objects with descriptive messages when validation fails:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
try {
|
|
278
|
+
await script.run(redis, { args: { limit: -1 } })
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof Error) {
|
|
281
|
+
console.error(error.message)
|
|
282
|
+
// "[upstash-lua@0.2.0] Script \"rateLimit\" input validation failed at \"args.limit\": Number must be positive"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Error messages include:
|
|
288
|
+
- The library version
|
|
289
|
+
- The script name
|
|
290
|
+
- The validation path (for input errors)
|
|
291
|
+
- The validation error messages
|
|
292
|
+
|
|
293
|
+
## How It Works
|
|
294
|
+
|
|
295
|
+
1. **Define** - Creates script with metadata and computed SHA1
|
|
296
|
+
- If `lua` is a function, it's called with typed `KEYS`/`ARGV` proxies
|
|
297
|
+
- The `lua` template is compiled: `${KEYS.name}` → `KEYS[n]`, `${ARGV.name}` → `ARGV[n]`
|
|
298
|
+
2. **Validate** - Keys/args are validated against StandardSchemaV1 schemas
|
|
299
|
+
3. **Transform** - Validated values are transformed (e.g., numbers → strings)
|
|
300
|
+
4. **Execute** - Uses EVALSHA for efficiency, falls back to SCRIPT LOAD on NOSCRIPT
|
|
301
|
+
5. **Parse** - Return value is validated/transformed if schema provided
|
|
302
|
+
|
|
303
|
+
The library caches script loading per-client, so concurrent calls share a single SCRIPT LOAD.
|
|
304
|
+
|
|
305
|
+
## Versioning
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { VERSION } from "upstash-lua"
|
|
309
|
+
console.log(`Using upstash-lua v${VERSION}`)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Errors include the version for debugging:
|
|
313
|
+
```
|
|
314
|
+
[upstash-lua@0.1.0] Script "rateLimit" input validation failed at "args.limit": ...
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { RedisLike } from "./redis-like.ts";
|
|
2
|
+
import type { AnyStandardSchema, ScriptCallArgs, StdOutput, StringSchemaRecord } from "./types.ts";
|
|
3
|
+
import { type CompiledLua, type TokenProxy } from "./lua-template.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Represents a defined Lua script with type-safe execution methods.
|
|
6
|
+
*
|
|
7
|
+
* The Script object is created by `defineScript()` and provides methods
|
|
8
|
+
* to execute the script with validated inputs and typed outputs.
|
|
9
|
+
*
|
|
10
|
+
* @typeParam K - The keys schema record
|
|
11
|
+
* @typeParam A - The args schema record
|
|
12
|
+
* @typeParam R - The return type after validation
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const myScript: Script<{ key: z.ZodString }, { limit: z.ZodNumber }, number> = defineScript({
|
|
17
|
+
* name: "myScript",
|
|
18
|
+
* lua: `return redis.call("INCR", KEYS[1])`,
|
|
19
|
+
* keys: { key: z.string() },
|
|
20
|
+
* args: { limit: z.number().transform(String) },
|
|
21
|
+
* returns: z.number(),
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
export interface Script<K extends StringSchemaRecord, A extends StringSchemaRecord, R> {
|
|
28
|
+
/**
|
|
29
|
+
* Human-readable name of the script.
|
|
30
|
+
* Used in error messages for debugging.
|
|
31
|
+
*/
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/**
|
|
34
|
+
* The Lua script source code.
|
|
35
|
+
*/
|
|
36
|
+
readonly lua: string;
|
|
37
|
+
/**
|
|
38
|
+
* Ordered list of key names.
|
|
39
|
+
* The order matches KEYS[1], KEYS[2], etc. in the Lua script.
|
|
40
|
+
* Derived from `Object.keys(def.keys)` at definition time.
|
|
41
|
+
*/
|
|
42
|
+
readonly keyNames: readonly Extract<keyof K, string>[];
|
|
43
|
+
/**
|
|
44
|
+
* Ordered list of argument names.
|
|
45
|
+
* The order matches ARGV[1], ARGV[2], etc. in the Lua script.
|
|
46
|
+
* Derived from `Object.keys(def.args)` at definition time.
|
|
47
|
+
*/
|
|
48
|
+
readonly argNames: readonly Extract<keyof A, string>[];
|
|
49
|
+
/**
|
|
50
|
+
* Executes the script without validating the return value.
|
|
51
|
+
*
|
|
52
|
+
* Use this when you don't need return validation or want to handle
|
|
53
|
+
* the raw Redis response yourself.
|
|
54
|
+
*
|
|
55
|
+
* @param redis - The Redis client to execute on
|
|
56
|
+
* @param input - The keys and args (omit if both are empty)
|
|
57
|
+
* @returns Promise resolving to the raw Redis response
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const raw = await myScript.runRaw(redis, {
|
|
62
|
+
* keys: { key: "test" },
|
|
63
|
+
* args: { limit: 10 },
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
runRaw(redis: RedisLike, ...input: ScriptCallArgs<K, A>): Promise<unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* Executes the script with full input and return validation.
|
|
72
|
+
*
|
|
73
|
+
* This is the primary method for executing scripts. It:
|
|
74
|
+
* 1. Validates all keys and args against their schemas
|
|
75
|
+
* 2. Transforms validated values (e.g., numbers to strings)
|
|
76
|
+
* 3. Executes the script via EVALSHA (with NOSCRIPT fallback)
|
|
77
|
+
* 4. Validates and transforms the return value (if returns schema provided)
|
|
78
|
+
*
|
|
79
|
+
* @param redis - The Redis client to execute on
|
|
80
|
+
* @param input - The keys and args (omit if both are empty)
|
|
81
|
+
* @returns Promise resolving to the validated and transformed return value
|
|
82
|
+
* @throws {Error} When validation fails
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const result = await myScript.run(redis, {
|
|
87
|
+
* keys: { key: "test" },
|
|
88
|
+
* args: { limit: 10 },
|
|
89
|
+
* })
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @since 0.1.0
|
|
93
|
+
*/
|
|
94
|
+
run(redis: RedisLike, ...input: ScriptCallArgs<K, A>): Promise<R>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Function type for type-safe Lua script definition.
|
|
98
|
+
*
|
|
99
|
+
* Receives typed `KEYS` and `ARGV` proxy objects and returns a `CompiledLua`
|
|
100
|
+
* template created with the `lua` tagged template function.
|
|
101
|
+
*
|
|
102
|
+
* @typeParam K - The keys schema record
|
|
103
|
+
* @typeParam A - The args schema record
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* lua: ({ KEYS, ARGV }) => lua`
|
|
108
|
+
* local key = ${KEYS.userKey}
|
|
109
|
+
* local limit = tonumber(${ARGV.limit})
|
|
110
|
+
* return redis.call("GET", key)
|
|
111
|
+
* `
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @since 0.2.0
|
|
115
|
+
*/
|
|
116
|
+
export type LuaFunction<K extends StringSchemaRecord, A extends StringSchemaRecord> = (ctx: {
|
|
117
|
+
KEYS: TokenProxy<K>;
|
|
118
|
+
ARGV: TokenProxy<A>;
|
|
119
|
+
}) => CompiledLua;
|
|
120
|
+
/**
|
|
121
|
+
* Base definition for a Lua script.
|
|
122
|
+
*
|
|
123
|
+
* @typeParam K - The keys schema record
|
|
124
|
+
* @typeParam A - The args schema record
|
|
125
|
+
*
|
|
126
|
+
* @since 0.1.0
|
|
127
|
+
*/
|
|
128
|
+
export interface DefineScriptBase<K extends StringSchemaRecord, A extends StringSchemaRecord> {
|
|
129
|
+
/**
|
|
130
|
+
* Human-readable name for the script.
|
|
131
|
+
* Used in error messages for debugging.
|
|
132
|
+
*/
|
|
133
|
+
name: string;
|
|
134
|
+
/**
|
|
135
|
+
* The Lua script source code.
|
|
136
|
+
*
|
|
137
|
+
* Can be either:
|
|
138
|
+
* - A plain string (for raw `.lua` files or manual scripts)
|
|
139
|
+
* - A function receiving typed `KEYS`/`ARGV` proxies and returning a `lua` template
|
|
140
|
+
*
|
|
141
|
+
* When using the function form, you get:
|
|
142
|
+
* - Autocomplete for `KEYS.*` and `ARGV.*`
|
|
143
|
+
* - Compile-time errors for invalid key/arg references
|
|
144
|
+
* - Automatic compilation of `${KEYS.name}` to `KEYS[n]`
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* // String form (no type safety for KEYS/ARGV):
|
|
149
|
+
* lua: `return redis.call("GET", KEYS[1])`
|
|
150
|
+
*
|
|
151
|
+
* // Function form (type-safe):
|
|
152
|
+
* lua: ({ KEYS, ARGV }) => lua`
|
|
153
|
+
* local key = ${KEYS.userKey}
|
|
154
|
+
* return redis.call("GET", key)
|
|
155
|
+
* `
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
lua: string | LuaFunction<K, A>;
|
|
159
|
+
/**
|
|
160
|
+
* Record of key schemas.
|
|
161
|
+
*
|
|
162
|
+
* **Important:** The order of keys in this object determines the order
|
|
163
|
+
* they appear in KEYS[1], KEYS[2], etc. Define keys using object literal
|
|
164
|
+
* syntax in the intended order. Do not spread from unknown sources.
|
|
165
|
+
*
|
|
166
|
+
* Each schema must output a string (use `.transform(String)` if needed).
|
|
167
|
+
*/
|
|
168
|
+
keys?: K;
|
|
169
|
+
/**
|
|
170
|
+
* Record of argument schemas.
|
|
171
|
+
*
|
|
172
|
+
* **Important:** The order of args in this object determines the order
|
|
173
|
+
* they appear in ARGV[1], ARGV[2], etc. Define args using object literal
|
|
174
|
+
* syntax in the intended order. Do not spread from unknown sources.
|
|
175
|
+
*
|
|
176
|
+
* Each schema must output a string (use `.transform(String)` if needed).
|
|
177
|
+
*/
|
|
178
|
+
args?: A;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Defines a type-safe Lua script for execution on Upstash Redis.
|
|
182
|
+
*
|
|
183
|
+
* This function creates a Script object that:
|
|
184
|
+
* - Validates keys and args using StandardSchemaV1 schemas
|
|
185
|
+
* - Transforms values (e.g., numbers to strings for Redis)
|
|
186
|
+
* - Executes efficiently via EVALSHA with automatic NOSCRIPT fallback
|
|
187
|
+
* - Validates and transforms return values
|
|
188
|
+
*
|
|
189
|
+
* **Key ordering:** The order of keys/args in the definition object determines
|
|
190
|
+
* their order in KEYS[1], KEYS[2] and ARGV[1], ARGV[2], etc. Always define
|
|
191
|
+
* using object literal syntax in the intended order.
|
|
192
|
+
*
|
|
193
|
+
* @param def - Script definition with name, lua, keys, args, and optional returns
|
|
194
|
+
* @returns A Script object with `run()` and `runRaw()` methods
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { z } from "zod"
|
|
199
|
+
* import { defineScript } from "upstash-lua"
|
|
200
|
+
*
|
|
201
|
+
* const rateLimit = defineScript({
|
|
202
|
+
* name: "rateLimit",
|
|
203
|
+
* lua: `
|
|
204
|
+
* local current = redis.call("INCR", KEYS[1])
|
|
205
|
+
* if current == 1 then
|
|
206
|
+
* redis.call("EXPIRE", KEYS[1], ARGV[2])
|
|
207
|
+
* end
|
|
208
|
+
* local allowed = current <= tonumber(ARGV[1]) and 1 or 0
|
|
209
|
+
* return { allowed, tonumber(ARGV[1]) - current }
|
|
210
|
+
* `,
|
|
211
|
+
* keys: {
|
|
212
|
+
* key: z.string(),
|
|
213
|
+
* },
|
|
214
|
+
* args: {
|
|
215
|
+
* limit: z.number().int().positive().transform(String),
|
|
216
|
+
* windowSeconds: z.number().int().positive().transform(String),
|
|
217
|
+
* },
|
|
218
|
+
* returns: z.tuple([z.number(), z.number()]).transform(([allowed, rem]) => ({
|
|
219
|
+
* allowed: allowed === 1,
|
|
220
|
+
* remaining: rem,
|
|
221
|
+
* })),
|
|
222
|
+
* })
|
|
223
|
+
*
|
|
224
|
+
* const result = await rateLimit.run(redis, {
|
|
225
|
+
* keys: { key: "rl:user:123" },
|
|
226
|
+
* args: { limit: 10, windowSeconds: 60 },
|
|
227
|
+
* })
|
|
228
|
+
* // result: { allowed: boolean, remaining: number }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @see https://github.com/your-org/upstash-lua for full documentation
|
|
232
|
+
* @since 0.1.0
|
|
233
|
+
*/
|
|
234
|
+
export declare function defineScript<const K extends StringSchemaRecord, const A extends StringSchemaRecord, const Ret extends AnyStandardSchema>(def: DefineScriptBase<K, A> & {
|
|
235
|
+
returns: Ret;
|
|
236
|
+
}): Script<K, A, StdOutput<Ret>>;
|
|
237
|
+
/**
|
|
238
|
+
* Defines a Lua script without return validation.
|
|
239
|
+
*
|
|
240
|
+
* When no `returns` schema is provided, `run()` returns `unknown`.
|
|
241
|
+
*
|
|
242
|
+
* @param def - Script definition with name, lua, keys, and args
|
|
243
|
+
* @returns A Script object with `run()` returning `unknown`
|
|
244
|
+
*
|
|
245
|
+
* @since 0.1.0
|
|
246
|
+
*/
|
|
247
|
+
export declare function defineScript<const K extends StringSchemaRecord, const A extends StringSchemaRecord>(def: DefineScriptBase<K, A> & {
|
|
248
|
+
returns?: undefined;
|
|
249
|
+
}): Script<K, A, unknown>;
|
|
250
|
+
//# sourceMappingURL=define-script.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-script.d.ts","sourceRoot":"","sources":["../src/define-script.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EAEd,SAAS,EACT,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAInB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EAIhB,MAAM,mBAAmB,CAAA;AAE1B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,kBAAkB,EAAE,CAAC,SAAS,kBAAkB,EAAE,CAAC;IACnF;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IAEpB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAA;IAEtD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAA;IAEtD;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAE1E;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CAClE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,kBAAkB,EAAE,CAAC,SAAS,kBAAkB,IAAI,CAAC,GAAG,EAAE;IAC1F,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IACnB,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;CACpB,KAAK,WAAW,CAAA;AAEjB;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,kBAAkB,EAAE,CAAC,SAAS,kBAAkB;IAC1F;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAE/B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,CAAC,CAAA;IAER;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,CAAC,CAAA;CACT;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,YAAY,CAC1B,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAClC,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAClC,KAAK,CAAC,GAAG,SAAS,iBAAiB,EACnC,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;AAE/E;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAClC,KAAK,CAAC,CAAC,SAAS,kBAAkB,EAClC,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a validation issue from a StandardSchemaV1 schema.
|
|
3
|
+
* This is a simplified representation that works across different schema libraries.
|
|
4
|
+
*
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
export interface ValidationIssue {
|
|
8
|
+
/** Human-readable error message */
|
|
9
|
+
readonly message: string;
|
|
10
|
+
/** Path to the invalid value within the input */
|
|
11
|
+
readonly path?: ReadonlyArray<PropertyKey>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when script input validation fails.
|
|
15
|
+
*
|
|
16
|
+
* This error is thrown when keys or args fail to validate against their
|
|
17
|
+
* respective StandardSchemaV1 schemas before the script is executed.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* try {
|
|
22
|
+
* await myScript.run(redis, { keys: { key: "" }, args: { limit: -1 } })
|
|
23
|
+
* } catch (error) {
|
|
24
|
+
* if (error instanceof ScriptInputError) {
|
|
25
|
+
* console.error(`Script: ${error.scriptName}`)
|
|
26
|
+
* console.error(`Field: ${error.path}`) // e.g., "args.limit"
|
|
27
|
+
* console.error(`Issues:`, error.issues)
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
*/
|
|
34
|
+
export declare class ScriptInputError extends Error {
|
|
35
|
+
/**
|
|
36
|
+
* The name of the script that failed validation.
|
|
37
|
+
* Matches the `name` property in the script definition.
|
|
38
|
+
*/
|
|
39
|
+
readonly scriptName: string;
|
|
40
|
+
/**
|
|
41
|
+
* The path to the invalid field.
|
|
42
|
+
* Format: `"keys.<fieldName>"` or `"args.<fieldName>"`
|
|
43
|
+
*
|
|
44
|
+
* @example "args.limit", "keys.userId"
|
|
45
|
+
*/
|
|
46
|
+
readonly path: string;
|
|
47
|
+
/**
|
|
48
|
+
* Array of validation issues from the schema library.
|
|
49
|
+
* Each issue contains at least a `message` property.
|
|
50
|
+
*/
|
|
51
|
+
readonly issues: readonly ValidationIssue[];
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new ScriptInputError.
|
|
54
|
+
*
|
|
55
|
+
* @param scriptName - Name of the script that failed validation
|
|
56
|
+
* @param path - Path to the invalid field (e.g., "args.limit")
|
|
57
|
+
* @param issues - Array of validation issues from the schema
|
|
58
|
+
*/
|
|
59
|
+
constructor(scriptName: string, path: string, issues: readonly ValidationIssue[]);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Error thrown when script return value validation fails.
|
|
63
|
+
*
|
|
64
|
+
* This error is thrown when the Redis response fails to validate against
|
|
65
|
+
* the `returns` StandardSchemaV1 schema after script execution.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* try {
|
|
70
|
+
* await myScript.run(redis, { keys: { key: "test" } })
|
|
71
|
+
* } catch (error) {
|
|
72
|
+
* if (error instanceof ScriptReturnError) {
|
|
73
|
+
* console.error(`Script: ${error.scriptName}`)
|
|
74
|
+
* console.error(`Raw response:`, error.raw)
|
|
75
|
+
* console.error(`Issues:`, error.issues)
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @since 0.1.0
|
|
81
|
+
*/
|
|
82
|
+
export declare class ScriptReturnError extends Error {
|
|
83
|
+
/**
|
|
84
|
+
* The name of the script whose return validation failed.
|
|
85
|
+
* Matches the `name` property in the script definition.
|
|
86
|
+
*/
|
|
87
|
+
readonly scriptName: string;
|
|
88
|
+
/**
|
|
89
|
+
* Array of validation issues from the schema library.
|
|
90
|
+
* Each issue contains at least a `message` property.
|
|
91
|
+
*/
|
|
92
|
+
readonly issues: readonly ValidationIssue[];
|
|
93
|
+
/**
|
|
94
|
+
* The raw value returned from Redis before validation.
|
|
95
|
+
* Useful for debugging schema mismatches.
|
|
96
|
+
*/
|
|
97
|
+
readonly raw: unknown;
|
|
98
|
+
/**
|
|
99
|
+
* Creates a new ScriptReturnError.
|
|
100
|
+
*
|
|
101
|
+
* @param scriptName - Name of the script that failed validation
|
|
102
|
+
* @param issues - Array of validation issues from the schema
|
|
103
|
+
* @param raw - The raw Redis response that failed validation
|
|
104
|
+
*/
|
|
105
|
+
constructor(scriptName: string, issues: readonly ValidationIssue[], raw: unknown);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAE3B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;IAE3C;;;;;;OAMG;gBAED,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,eAAe,EAAE;CAiBrC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAE3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;IAE3C;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IAErB;;;;;;OAMG;gBAED,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,GAAG,EAAE,OAAO;CAiBf"}
|