diff --git a/modules/rate_limit/README.md b/modules/rate_limit/README.md
new file mode 100644
index 0000000..4230450
--- /dev/null
+++ b/modules/rate_limit/README.md
@@ -0,0 +1,172 @@
+# Rate Limit
+
+Rate limiting is a crucial aspect of managing your game's backend to prevent abuse and ensure fair usage of your resources. The `rate_limit` module in Open Game Backend provides an easy way to implement rate limiting for your scripts.
+
+## What is Rate Limiting?
+
+Rate limiting restricts the number of requests a client can make to your backend within a specified time period. This helps to:
+
+1. Prevent abuse and potential DoS attacks
+2. Ensure fair usage of resources among all users
+3. Manage server load and maintain performance
+
+## Using the Rate Limit Module
+
+The `rate_limit` module provides two main scripts:
+
+1. `throttle`: For general-purpose rate limiting
+2. `throttle_public`: Specifically designed for rate limiting public scripts
+
+Let's focus on `throttle_public` as it's the recommended method for rate limiting public scripts.
+
+### Throttling Public Scripts
+
+To rate limit a public script, you can use the `throttle_public` script at the beginning of your script. Here's how to use it:
+
+
+```typescript scripts/my_public_script.ts
+import { ScriptContext } from "../module.gen.ts";
+
+export interface Request {
+ // Your request parameters here
+}
+
+export interface Response {
+ // Your response type here
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request
+): Promise {
+ // Apply rate limiting
+ await ctx.modules.rateLimit.throttlePublic({});
+
+ // Your script logic here
+ // ...
+
+ return {
+ // Your response here
+ };
+}
+```
+
+
+The `throttlePublic` script automatically uses the client's IP address for rate limiting. By default, it allows 20 requests per 5 minutes (300 seconds) for each IP address.
+
+### Customizing Rate Limits
+
+You can customize the rate limit by passing parameters to `throttlePublic`:
+
+
+```typescript scripts/my_custom_rate_limited_script.ts
+export async function run(
+ ctx: ScriptContext,
+ req: Request
+): Promise {
+ // Custom rate limit: 5 requests per 1 minute
+ await ctx.modules.rateLimit.throttlePublic({
+ requests: 5,
+ period: 60
+ });
+
+ // Your script logic here
+ // ...
+}
+```
+
+
+## Best Practices for Rate Limiting
+
+1. **Only Rate Limit Public Scripts**: Apply rate limiting to public scripts that are directly accessible by clients. Internal scripts called by other modules typically don't need rate limiting.
+
+2. **Choose Appropriate Limits**: Set rate limits that balance protection against abuse with legitimate usage patterns. Consider your game's requirements and user behavior.
+
+3. **Use `throttle_public` for Client-Facing Scripts**: The `throttle_public` script is designed to work with client IP addresses automatically, making it ideal for public-facing endpoints.
+
+4. **Consistent Application**: Apply rate limiting consistently across all public endpoints to prevent attackers from finding unprotected routes.
+
+5. **Monitor and Adjust**: Regularly review your rate limits and adjust them based on real-world usage and any issues that arise.
+
+6. **Inform Users**: When a rate limit is exceeded, return a clear error message to the client so they understand why their request was blocked.
+
+7. **Consider Different Limits for Different Actions**: Some actions might require stricter rate limits than others. For example:
+
+
+ ```typescript scripts/login.ts
+ export async function run(ctx: ScriptContext, req: Request): Promise {
+ // Stricter rate limit for login attempts
+ await ctx.modules.rateLimit.throttlePublic({
+ requests: 5,
+ period: 300 // 5 minutes
+ });
+
+ // Login logic here
+ // ...
+ }
+ ```
+
+ ```typescript scripts/fetch_game_state.ts
+ export async function run(ctx: ScriptContext, req: Request): Promise {
+ // More lenient rate limit for fetching game state
+ await ctx.modules.rateLimit.throttlePublic({
+ requests: 60,
+ period: 60 // 1 minute
+ });
+
+ // Fetch game state logic here
+ // ...
+ }
+ ```
+
+
+8. **Handle Rate Limit Errors Gracefully**: In your client-side code, be prepared to handle rate limit errors and potentially implement exponential backoff for retries.
+
+## Example: Rate Limited Leaderboard Fetch
+
+Here's an example of how you might implement a rate-limited public script for fetching a game leaderboard:
+
+
+```typescript scripts/fetch_leaderboard.ts
+import { ScriptContext } from "../module.gen.ts";
+
+export interface Request {
+ gameMode: string;
+ limit: number;
+}
+
+export interface Response {
+ leaderboard: LeaderboardEntry[];
+}
+
+interface LeaderboardEntry {
+ username: string;
+ score: number;
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request
+): Promise {
+ // Apply rate limiting: 10 requests per minute
+ await ctx.modules.rateLimit.throttlePublic({
+ requests: 10,
+ period: 60
+ });
+
+ // Validate request
+ if (req.limit > 100) {
+ throw new Error("Limit cannot exceed 100");
+ }
+
+ // Fetch leaderboard logic here (example)
+ const leaderboard = await ctx.modules.gameData.getLeaderboard(req.gameMode, req.limit);
+
+ return {
+ leaderboard
+ };
+}
+```
+
+
+In this example, we've applied a rate limit of 10 requests per minute to the leaderboard fetch script. This allows frequent updates for players while still protecting the backend from excessive requests.
diff --git a/modules/tokens/README.md b/modules/tokens/README.md
index bd567f2..b6874a7 100644
--- a/modules/tokens/README.md
+++ b/modules/tokens/README.md
@@ -1,4 +1,383 @@
-# tokens
+# Tokens
+
+The token module allows you to create, validate, fetch, and revoke tokens. These tokens are typically used for authentication and authorization purposes in your game backend.
+
+## Common Use Cases
+
+1. **User Authentication**: Create tokens when users log in and validate them on subsequent requests.
+2. **Session Management**: Use tokens to maintain user sessions across multiple requests.
+3. **API Access**: Generate tokens for third-party applications to access your game's API.
+4. **Password Reset**: Create short-lived tokens for password reset functionality.
+5. **Email Verification**: Generate tokens to verify user email addresses.
+
+## Token Parameters
+
+When creating or working with tokens, you'll encounter the following parameters:
+
+1. `type` (string): Identifies the purpose of the token (e.g., "user", "api", "reset_password").
+2. `meta` (object): Additional metadata associated with the token.
+3. `expireAt` (string, optional): ISO 8601 timestamp for when the token should expire.
+4. `token` (string): The actual token string used for authentication.
+5. `id` (string): A unique identifier for the token.
+6. `createdAt` (string): ISO 8601 timestamp of when the token was created.
+7. `revokedAt` (string, optional): ISO 8601 timestamp of when the token was revoked, if applicable.
+
+## Functionality
+
+The token module provides the following core functions:
+
+### 1. Create Token
+
+Creates a new token with the specified parameters.
+
+
+```typescript scripts/create.ts
+export interface Request {
+ type: string;
+ meta: Record;
+ expireAt?: string;
+}
+
+export interface Response {
+ token: TokenWithSecret;
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const tokenStr = generateToken(req.type);
+
+ const rows = await ctx.db.insert(Database.tokens)
+ .values({
+ token: tokenStr,
+ type: req.type,
+ meta: req.meta,
+ trace: ctx.trace,
+ expireAt: req.expireAt ? new Date(req.expireAt) : undefined,
+ })
+ .returning();
+
+ return {
+ token: tokenFromRow(rows[0]!),
+ };
+}
+```
+
+
+### 2. Validate Token
+
+Validates a given token, checking if it exists and hasn't expired or been revoked.
+
+
+```typescript scripts/validate.ts
+export interface Request {
+ token: string;
+}
+
+export interface Response {
+ token: Token;
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const { tokens } = await ctx.modules.tokens.fetchByToken({
+ tokens: [req.token],
+ });
+ const token = tokens[0];
+
+ if (!token) throw new RuntimeError("token_not_found");
+
+ if (token.revokedAt) throw new RuntimeError("token_revoked");
+
+ if (token.expireAt) {
+ const expireAt = new Date(token.expireAt);
+ const now = new Date();
+ if (expireAt < now) {
+ throw new RuntimeError("token_expired");
+ }
+ }
+
+ return { token };
+}
+```
+
+
+### 3. Fetch Tokens
+
+Retrieves token information based on token IDs.
+
+
+```typescript scripts/fetch.ts
+export interface Request {
+ tokenIds: string[];
+}
+
+export interface Response {
+ tokens: Token[];
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const rows = await ctx.db.query.tokens.findMany({
+ where: Query.inArray(Database.tokens.id, req.tokenIds),
+ orderBy: [Query.desc(Database.tokens.createdAt)]
+ });
+
+ const tokens = rows.map(tokenFromRow);
+
+ return { tokens };
+}
+```
+
+
+### 4. Fetch by Token
+
+Retrieves token information based on the token strings.
+
+
+```typescript scripts/fetch_by_token.ts
+export interface Request {
+ tokens: string[];
+}
+
+export interface Response {
+ tokens: Token[];
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ const rows = await ctx.db.query.tokens.findMany({
+ where: Query.inArray(Database.tokens.token, req.tokens),
+ orderBy: [Query.desc(Database.tokens.createdAt)]
+ });
+
+ const tokens = rows.map(tokenFromRow);
+
+ return { tokens };
+}
+```
+
+
+### 5. Revoke Tokens
+
+Revokes one or more tokens, preventing their further use.
+
+
+```typescript scripts/revoke.ts
+export interface Request {
+ tokenIds: string[];
+}
+
+export interface Response {
+ updates: { [key: string]: TokenUpdate };
+}
+
+export enum TokenUpdate {
+ Revoked = "revoked",
+ AlreadyRevoked = "already_revoked",
+ NotFound = "not_found",
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ interface TokenRow {
+ id: string;
+ already_revoked: boolean;
+ }
+
+ // Sets revokedAt on all tokens that have not already been revoked. Returns
+ // whether or not each token was revoked.
+ const { rows }: { rows: TokenRow[] } = await ctx.db.execute(Query.sql`
+ WITH pre_update AS (
+ SELECT
+ ${Database.tokens.id} AS id,
+ ${Database.tokens.revokedAt} AS revoked_at
+ FROM ${Database.tokens}
+ WHERE ${Database.tokens.id} IN ${req.tokenIds}
+ )
+ UPDATE ${Database.tokens}
+ SET revoked_at = COALESCE(${Database.tokens.revokedAt}, current_timestamp)
+ FROM pre_update
+ WHERE ${Database.tokens.id} = pre_update.id
+ RETURNING
+ ${Database.tokens.id} AS id,
+ pre_update.revoked_at IS NOT NULL AS already_revoked
+ `);
+
+ const updates: Record = {};
+ for (const tokenId of req.tokenIds) {
+ const tokenRow = rows.find((row) => row.id === tokenId);
+ if (tokenRow) {
+ updates[tokenId] = tokenRow.already_revoked
+ ? TokenUpdate.AlreadyRevoked
+ : TokenUpdate.Revoked;
+ } else {
+ updates[tokenId] = TokenUpdate.NotFound;
+ }
+ }
+
+ return { updates };
+}
+```
+
+
+### 6. Extend Token
+
+Extends the expiration date of a token.
+
+
+```typescript scripts/extend.ts
+export interface Request {
+ token: string;
+ newExpiration: string | null;
+}
+
+export interface Response {
+ token: TokenWithSecret;
+}
+
+export async function run(
+ ctx: ScriptContext,
+ req: Request,
+): Promise {
+ // Ensure the token hasn't expired or been revoked yet
+ const { token } = await ctx.modules.tokens.validate({
+ token: req.token,
+ });
+
+ // Update the token's expiration date
+ const rows = await ctx.db.update(Database.tokens)
+ .set({ expireAt: req.newExpiration ? new Date(req.newExpiration) : null })
+ .where(Query.eq(Database.tokens.id, token.id))
+ .returning();
+
+ // Return the updated token
+ return {
+ token: tokenFromRow(rows[0]!),
+ };
+}
+```
+
+
+## Errors
+
+The token module can throw the following errors:
+
+1. `token_not_found`: The specified token does not exist.
+2. `token_revoked`: The token has been revoked and is no longer valid.
+3. `token_expired`: The token has expired and is no longer valid.
+
+## Common Patterns
+
+### 1. Creating and Using User Tokens
+
+When a user logs in, create a token and return it to the client:
+
+```typescript
+const { token } = await ctx.modules.tokens.create({
+ type: "user",
+ meta: { userId: user.id },
+ expireAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
+});
+
+// Return token to client
+return { userToken: token.token };
+```
+
+On subsequent requests, validate the token:
+
+```typescript
+const { token } = await ctx.modules.tokens.validate({
+ token: req.userToken,
+});
+
+// Use token.meta.userId to identify the user
+const userId = token.meta.userId;
+```
+
+### 2. Passing Tokens in Requests
+
+Tokens are typically passed in the `Authorization` header of HTTP requests:
+
+```http
+Authorization: Bearer
+```
+
+In your route handlers, you can extract and validate the token:
+
+```typescript
+export async function run(ctx: RouteContext, req: Request): Promise {
+ const authHeader = req.headers.get("Authorization");
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ throw new RuntimeError("invalid_auth_header");
+ }
+
+ const token = authHeader.slice(7); // Remove "Bearer " prefix
+
+ try {
+ const { token: validatedToken } = await ctx.modules.tokens.validate({ token });
+ // Use validatedToken.meta to access token metadata
+ } catch (error) {
+ if (error instanceof RuntimeError && error.code === "token_not_found") {
+ throw new RuntimeError("unauthorized");
+ }
+ throw error;
+ }
+
+ // Proceed with the authenticated request
+}
+```
+
+### 3. Revoking Tokens on Logout
+
+When a user logs out, revoke their token:
+
+```typescript
+await ctx.modules.tokens.revoke({
+ tokenIds: [userToken.id],
+});
+```
+
+### 4. Using Short-lived Tokens for Password Reset
+
+Create a short-lived token for password reset:
+
+```typescript
+const { token } = await ctx.modules.tokens.create({
+ type: "password_reset",
+ meta: { userId: user.id },
+ expireAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour
+});
+
+// Send token to user's email
+await sendPasswordResetEmail(user.email, token.token);
+```
+
+When the user clicks the reset link, validate the token:
+
+```typescript
+const { token } = await ctx.modules.tokens.validate({
+ token: req.resetToken,
+});
+
+// Ensure token type is correct
+if (token.type !== "password_reset") {
+ throw new RuntimeError("invalid_token_type");
+}
+
+// Allow password reset for token.meta.userId
+```
+
+By following these patterns and utilizing the token module's functionality, you can implement secure authentication and authorization in your Open Game Backend projects.
## FAQ
diff --git a/modules/uploads/README.md b/modules/uploads/README.md
new file mode 100644
index 0000000..04c268d
--- /dev/null
+++ b/modules/uploads/README.md
@@ -0,0 +1,311 @@
+# Uploads
+
+The uploads module in Open Game Backend provides a robust system for handling file uploads in your game. It allows you to securely store and manage various types of files, such as user-generated content, game assets, or any other binary data your game might need to handle.
+
+## Overview
+
+The uploads module offers the following key features:
+
+1. Secure file uploading
+2. Support for both single-part and multi-part uploads
+3. File metadata management
+4. Presigned URLs for direct client-side uploads
+5. File retrieval and deletion
+
+## Getting Started
+
+To use the uploads module in your Open Game Backend project, you'll need to ensure it's properly configured in your `backend.json` file. Here's an example configuration:
+
+
+```json backend.json
+{
+ "modules": {
+ "uploads": {
+ "config": {
+ "maxUploadSize": "30mib",
+ "maxMultipartUploadSize": "10gib",
+ "maxFilesPerUpload": 10,
+ "defaultMultipartChunkSize": "10mib"
+ }
+ }
+ }
+}
+```
+
+
+Let's break down these configuration options:
+
+- `maxUploadSize`: The maximum size for a single-part upload (default: 30 MiB)
+- `maxMultipartUploadSize`: The maximum size for a multi-part upload (default: 10 GiB)
+- `maxFilesPerUpload`: The maximum number of files allowed in a single upload request (default: 10)
+- `defaultMultipartChunkSize`: The default chunk size for multi-part uploads (default: 10 MiB)
+
+## Core Concepts
+
+### Upload Process
+
+The upload process in the uploads module consists of three main steps:
+
+1. **Prepare**: Initialize the upload and get presigned URLs for uploading.
+2. **Upload**: Use the presigned URLs to upload file data directly to storage.
+3. **Complete**: Finalize the upload process.
+
+### Single-part vs Multi-part Uploads
+
+- **Single-part uploads** are suitable for smaller files (up to 30 MiB by default).
+- **Multi-part uploads** are used for larger files, allowing them to be uploaded in chunks.
+
+## API Reference
+
+### Prepare Upload
+
+Prepares an upload by creating metadata and generating presigned URLs for file uploads.
+
+
+```typescript
+async function prepareUpload(ctx: ScriptContext, req: {
+ files: {
+ path: string;
+ contentLength: string;
+ mime: string;
+ multipart: boolean;
+ }[];
+ metadata?: unknown;
+}): Promise<{
+ upload: PresignedUpload;
+}>
+```
+
+
+#### Parameters:
+
+- `files`: An array of file objects containing:
+ - `path`: The desired path/name for the file
+ - `contentLength`: The size of the file in bytes (as a string)
+ - `mime`: The MIME type of the file
+ - `multipart`: Whether to use multi-part upload (for large files)
+- `metadata`: Optional metadata to associate with the upload
+
+#### Returns:
+
+- `upload`: A `PresignedUpload` object containing upload details and presigned URLs for each file
+
+### Complete Upload
+
+Finalizes an upload after all files have been uploaded.
+
+
+```typescript
+async function completeUpload(ctx: ScriptContext, req: {
+ uploadId: string;
+}): Promise<{
+ upload: UploadWithoutFiles;
+}>
+```
+
+
+#### Parameters:
+
+- `uploadId`: The ID of the upload to complete
+
+#### Returns:
+
+- `upload`: An `UploadWithoutFiles` object containing upload details
+
+### Fetch Upload Metadata
+
+Retrieves metadata for one or more uploads.
+
+
+```typescript
+async function fetchUploadMetadata(ctx: ScriptContext, req: {
+ uploadIds: string[];
+ includeFiles?: boolean;
+}): Promise<{
+ uploads: UploadWithOptionalFiles[];
+}>
+```
+
+
+#### Parameters:
+
+- `uploadIds`: An array of upload IDs to fetch
+- `includeFiles`: Whether to include file details in the response (optional)
+
+#### Returns:
+
+- `uploads`: An array of `UploadWithOptionalFiles` objects containing upload details
+
+### Fetch Public File URLs
+
+Generates public URLs for accessing uploaded files.
+
+
+```typescript
+async function fetchPublicFileUrls(ctx: ScriptContext, req: {
+ uploadId: string;
+ filePaths: string[];
+ expirySeconds?: number;
+}): Promise<{
+ files: DownloadableFile[];
+}>
+```
+
+
+#### Parameters:
+
+- `uploadId`: The ID of the upload containing the files
+- `filePaths`: An array of file paths to generate URLs for
+- `expirySeconds`: The number of seconds the URLs should remain valid (optional)
+
+#### Returns:
+
+- `files`: An array of `DownloadableFile` objects containing file details and public URLs
+
+### Delete Upload
+
+Removes an upload and its associated files from storage.
+
+
+```typescript
+async function deleteUpload(ctx: ScriptContext, req: {
+ uploadId: string;
+}): Promise<{
+ bytesDeleted: string;
+}>
+```
+
+
+#### Parameters:
+
+- `uploadId`: The ID of the upload to delete
+
+#### Returns:
+
+- `bytesDeleted`: The number of bytes deleted (as a string)
+
+## Usage Examples
+
+### Preparing and Completing a Single-part Upload
+
+Here's an example of how to prepare and complete a single-part upload:
+
+
+```typescript
+import { ScriptContext } from "opengb";
+
+export async function uploadProfilePicture(ctx: ScriptContext, req: {
+ userId: string;
+ fileName: string;
+ fileSize: number;
+ mimeType: string;
+}) {
+ // Prepare the upload
+ const { upload } = await ctx.modules.uploads.prepare({
+ files: [{
+ path: `users/${req.userId}/profile-picture/${req.fileName}`,
+ contentLength: req.fileSize.toString(),
+ mime: req.mimeType,
+ multipart: false
+ }],
+ metadata: { userId: req.userId }
+ });
+
+ // Return the presigned URL to the client
+ const presignedUrl = upload.files[0].presignedChunks[0].url;
+
+ // The client would use this URL to upload the file directly
+
+ // After the client has uploaded the file, complete the upload
+ await ctx.modules.uploads.complete({ uploadId: upload.id });
+
+ return { message: "Profile picture uploaded successfully" };
+}
+```
+
+
+### Handling a Multi-part Upload
+
+For larger files, you'll want to use multi-part uploads. Here's an example:
+
+
+```typescript
+import { ScriptContext } from "opengb";
+
+export async function uploadLargeFile(ctx: ScriptContext, req: {
+ userId: string;
+ fileName: string;
+ fileSize: number;
+ mimeType: string;
+}) {
+ // Prepare the multi-part upload
+ const { upload } = await ctx.modules.uploads.prepare({
+ files: [{
+ path: `users/${req.userId}/large-files/${req.fileName}`,
+ contentLength: req.fileSize.toString(),
+ mime: req.mimeType,
+ multipart: true
+ }],
+ metadata: { userId: req.userId }
+ });
+
+ // Return the presigned URLs for each chunk to the client
+ const presignedChunks = upload.files[0].presignedChunks;
+
+ // The client would use these URLs to upload each chunk of the file
+
+ // After the client has uploaded all chunks, complete the upload
+ await ctx.modules.uploads.complete({ uploadId: upload.id });
+
+ return { message: "Large file uploaded successfully" };
+}
+```
+
+
+### Retrieving and Using Uploaded Files
+
+Here's how you can fetch and use the URLs for uploaded files:
+
+
+```typescript
+import { ScriptContext } from "opengb";
+
+export async function getProfilePictureUrl(ctx: ScriptContext, req: {
+ userId: string;
+ uploadId: string;
+}) {
+ const { files } = await ctx.modules.uploads.fetchPublicFileUrls({
+ uploadId: req.uploadId,
+ filePaths: [`users/${req.userId}/profile-picture`],
+ expirySeconds: 3600 // URL will be valid for 1 hour
+ });
+
+ if (files.length === 0) {
+ throw new Error("Profile picture not found");
+ }
+
+ return { profilePictureUrl: files[0].url };
+}
+```
+
+
+## Best Practices
+
+1. **Use multi-part uploads for large files**: This improves reliability and allows for resumable uploads.
+2. **Set appropriate expiry times for public URLs**: Balance security with usability when generating public file URLs.
+3. **Validate file types and sizes**: Implement checks on the client-side and server-side to ensure only allowed file types and sizes are uploaded.
+4. **Use meaningful file paths**: Organize your uploads with a clear directory structure (e.g., `users/{userId}/profile-pictures/{fileName}`).
+5. **Clean up unused uploads**: Implement a system to periodically delete unused or temporary uploads to manage storage efficiently.
+
+## Error Handling
+
+The uploads module can throw various errors. Here are some common ones to handle:
+
+- `no_files`: Thrown when trying to create an upload with no files.
+- `too_many_files`: Thrown when the number of files exceeds `maxFilesPerUpload`.
+- `duplicate_paths`: Thrown when two files in the same upload have the same path.
+- `size_limit_exceeded`: Thrown when the total upload size exceeds the configured limit.
+- `upload_not_found`: Thrown when trying to operate on a non-existent upload.
+- `upload_already_completed`: Thrown when trying to complete an already completed upload.
+
+Always wrap your upload operations in try-catch blocks and handle these errors gracefully in your game logic.
diff --git a/tests/basic/deno.lock b/tests/basic/deno.lock
index 5af0b04..969c62b 100644
--- a/tests/basic/deno.lock
+++ b/tests/basic/deno.lock
@@ -9,6 +9,7 @@
"npm:@types/node": "npm:@types/node@18.16.19",
"npm:drizzle-orm@0.33.0": "npm:drizzle-orm@0.33.0_pg@8.12.0",
"npm:pg@8.11.3": "npm:pg@8.11.3",
+ "npm:pg@8.12.0": "npm:pg@8.12.0",
"npm:pg@^8.11.3": "npm:pg@8.12.0",
"npm:tsup": "npm:tsup@8.2.4_typescript@5.5.4_esbuild@0.23.1",
"npm:typescript": "npm:typescript@5.5.4",
@@ -1166,11 +1167,42 @@
"https://deno.land/std@0.222.1/encoding/base64url.ts": "ef40e0f18315ab539f17cebcc32839779e018d86dea9df39d94d302f342a1713",
"https://deno.land/std@0.222.1/encoding/hex.ts": "6270f25e5d85f99fcf315278670ba012b04b7c94b67715b53f30d03249687c07",
"https://deno.land/std@0.222.1/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
+ "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
+ "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
+ "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
+ "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
+ "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
+ "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
+ "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
+ "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
+ "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
+ "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
+ "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
+ "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
+ "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
+ "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
+ "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
+ "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
+ "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
+ "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
+ "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
+ "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
+ "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
+ "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
+ "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
+ "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
+ "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
+ "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
+ "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
"https://deno.land/std@0.224.0/collections/_utils.ts": "b2ec8ada31b5a72ebb1d99774b849b4c09fe4b3a38d07794bd010bd218a16e0b",
"https://deno.land/std@0.224.0/collections/deep_merge.ts": "04f8d2a6cfa15c7580e788689bcb5e162512b9ccb18bab1241824b432a78551e",
"https://deno.land/std@0.224.0/crypto/timing_safe_equal.ts": "bc3622b5aec05e2d8b735bf60633425c34333c06cfb6c4a9f102e4a0f3931ced",
+ "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
+ "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
+ "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
+ "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
"https://deno.land/x/argontwo@0.2.0/mod.ts": "076395861fc86d5b475ed49ecf10a35df47db55a506469da054a92969d7cdf66",
"https://deno.land/x/argontwo@0.2.0/wasm/mod.ts": "588486da70ccd43760b71fb3d236ff37d1790d3419cbb764c35fc05a550a2881",
"https://deno.land/x/argontwo@0.2.0/wasm/wasm.js": "c5fdbe828b5984d9a434247ca2cf6cd44e8f7560490813cdd92be785ec57d3fa",
@@ -2501,6 +2533,8 @@
"https://esm.sh/v135/strnum@1.0.5/denonext/strnum.mjs": "1ffef4adec2f74139e36a2bfed8381880541396fe1c315779fb22e081b17468b",
"https://esm.sh/v135/tslib@2.6.2/denonext/tslib.mjs": "29782bcd3139f77ec063dc5a9385c0fff4a8d0a23b6765c73d9edeb169a04bf1",
"https://esm.sh/v135/tslib@2.6.3/denonext/tslib.mjs": "0834c22e9fbf95f6a5659cc2017543f7d41aa880f24ab84cb11d24e6bee99303",
- "https://esm.sh/v135/uuid@9.0.1/denonext/uuid.mjs": "7d7d3aa57fa136e2540886654c416d9da10d8cfebe408bae47fd47070f0bfb2a"
+ "https://esm.sh/v135/uuid@9.0.1/denonext/uuid.mjs": "7d7d3aa57fa136e2540886654c416d9da10d8cfebe408bae47fd47070f0bfb2a",
+ "https://esm.sh/zod-validation-error@3.3.0": "d8825ca67952b6adff6b35026dc465f9638d4923dbd54fe9e8e81fbfddca9630",
+ "https://esm.sh/zod@3.23.8": "728819c1f651800179a5a80daf24b3e54b2ddea87828bd10e63875a604bcb94e"
}
}