gh-as-db 1.1.1 โ 1.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 +109 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.js +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/infrastructure/github-storage.d.ts +9 -1
- package/dist/infrastructure/github-storage.js +183 -13
- package/dist/infrastructure/transaction-storage.d.ts +25 -0
- package/dist/infrastructure/transaction-storage.js +54 -0
- package/dist/ui/cli/index.js +0 -0
- package/dist/ui/collection.d.ts +11 -3
- package/dist/ui/collection.js +128 -19
- package/dist/ui/github-db.d.ts +5 -1
- package/dist/ui/github-db.js +7 -1
- package/dist/ui/transaction.d.ts +23 -0
- package/dist/ui/transaction.js +31 -0
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -11,14 +11,19 @@ Use a private GitHub repository as a database for your application. `gh-as-db` p
|
|
|
11
11
|
- ๐ **Secure**: Designed for private repositories using Personal Access Tokens (PAT).
|
|
12
12
|
- ๐ **Performance**: Built-in in-memory caching and auto-indexing for fast local queries.
|
|
13
13
|
- ๐ก๏ธ **Concurrency**: Optimistic locking using Git SHAs to prevent data loss.
|
|
14
|
+
- ๐ **Retry & Rate Limits**: Automatic retries with exponential backoff for transient errors and GitHub rate limits.
|
|
15
|
+
- ๐ **Transactions**: Group multiple operations into a single atomic Git commit.
|
|
16
|
+
- ๐ **Sharding**: One-file-per-document storage strategy for massive collections.
|
|
14
17
|
- ๐งฉ **Middleware**: Extensible hooks for data validation or transformation.
|
|
18
|
+
- ๐ **Validation**: Pluggable schema validation support (Zod, etc.).
|
|
19
|
+
- ๐ **Edge Ready**: Fully compatible with Vercel Edge and Cloudflare Workers.
|
|
15
20
|
- โจ๏ธ **CLI Tool**: Initialize and manage your "database" from the terminal.
|
|
16
21
|
- ๐ฆ **TypeScript**: Fully typed for a great developer experience.
|
|
17
22
|
|
|
18
23
|
## Installation
|
|
19
24
|
|
|
20
25
|
```bash
|
|
21
|
-
|
|
26
|
+
pnpm add gh-as-db
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
## Quick Start
|
|
@@ -58,6 +63,15 @@ const results = await users.find({
|
|
|
58
63
|
{ field: 'name', operator: 'eq', value: 'John Doe' }
|
|
59
64
|
]
|
|
60
65
|
});
|
|
66
|
+
|
|
67
|
+
// Use Transactions for atomic multi-collection updates
|
|
68
|
+
await db.transaction(async (tx) => {
|
|
69
|
+
const usersTx = tx.collection('users');
|
|
70
|
+
const logsTx = tx.collection('logs');
|
|
71
|
+
|
|
72
|
+
await usersTx.update('1', { name: 'Updated Name' });
|
|
73
|
+
await logsTx.create({ id: 'log-1', action: 'Update user' });
|
|
74
|
+
}, 'Atomic update of user and logs');
|
|
61
75
|
```
|
|
62
76
|
|
|
63
77
|
## API Reference
|
|
@@ -72,6 +86,7 @@ const db = new GitHubDB({
|
|
|
72
86
|
owner: string; // Repository owner
|
|
73
87
|
repo: string; // Repository name
|
|
74
88
|
cacheTTL?: number; // Optional: Cache TTL in ms. Default is 0 (strict consistency).
|
|
89
|
+
retry?: RetryConfig | false; // Optional: Retry config, or false to disable. See below.
|
|
75
90
|
});
|
|
76
91
|
```
|
|
77
92
|
|
|
@@ -131,6 +146,98 @@ const users = db.collection<User>('users', {
|
|
|
131
146
|
});
|
|
132
147
|
```
|
|
133
148
|
|
|
149
|
+
### Schema Validation
|
|
150
|
+
|
|
151
|
+
You can use any validation library (Zod, Valibot, etc.) by providing a `validator` object.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { z } from 'zod';
|
|
155
|
+
|
|
156
|
+
const userSchema = z.object({
|
|
157
|
+
id: z.string(),
|
|
158
|
+
name: z.string().min(3),
|
|
159
|
+
email: z.string().email(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const users = db.collection<User>('users', {
|
|
163
|
+
validator: {
|
|
164
|
+
validate: (data) => userSchema.parseAsync(data),
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Transactions & Batching
|
|
170
|
+
|
|
171
|
+
Transactions allow you to group multiple operations across different collections into a **single Git commit**. This reduces API calls and ensures that either all operations succeed or none are committed.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const commitSha = await db.transaction(async (tx) => {
|
|
175
|
+
const posts = tx.collection('posts');
|
|
176
|
+
const count = tx.collection('stats');
|
|
177
|
+
|
|
178
|
+
await posts.create({ id: 'p1', title: 'New Post' });
|
|
179
|
+
await count.update('total', { value: 101 });
|
|
180
|
+
}, 'Create post and increment counter');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Storage Strategies (Sharding)
|
|
184
|
+
|
|
185
|
+
By default, `gh-as-db` stores the entire collection in a single JSON file (`name.json`). For large collections, you can use the `sharded` strategy, which stores **one file per document** (`name/id.json`).
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const users = db.collection<User>('users', {
|
|
189
|
+
strategy: 'sharded'
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Why use Sharding?**
|
|
194
|
+
- ๐ **Performance**: `findById` reads only the specific file, which is much faster than loading a massive JSON array.
|
|
195
|
+
- ๐ **Scalability**: Avoids GitHub's file size limits and reduces merge conflicts.
|
|
196
|
+
- ๐งน **Cleanliness**: Better organization for repositories with thousands of documents.
|
|
197
|
+
|
|
198
|
+
### Retry & Rate Limit Handling
|
|
199
|
+
|
|
200
|
+
All GitHub API calls are automatically retried on transient errors (429 rate limits, 500/502/503 server errors) with exponential backoff. If GitHub returns a `Retry-After` header, it is respected.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const db = new GitHubDB({
|
|
204
|
+
accessToken: process.env.GITHUB_TOKEN,
|
|
205
|
+
owner: 'your-username',
|
|
206
|
+
repo: 'my-data-repo',
|
|
207
|
+
retry: {
|
|
208
|
+
maxRetries: 3, // Default: 3
|
|
209
|
+
baseDelay: 1000, // Default: 1000ms
|
|
210
|
+
maxDelay: 10000, // Default: 10000ms
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
To disable retries entirely:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const db = new GitHubDB({
|
|
219
|
+
// ...
|
|
220
|
+
retry: false,
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Non-transient errors (401, 403, 404) are thrown immediately without retrying. Concurrency conflicts (409) still throw `ConcurrencyError` immediately. If rate limit retries are exhausted, a `RateLimitError` is thrown.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { RateLimitError, ConcurrencyError } from 'gh-as-db';
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await users.create({ id: '1', name: 'Alice' });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof RateLimitError) {
|
|
233
|
+
console.log('Rate limited, retry after:', error.retryAfter);
|
|
234
|
+
}
|
|
235
|
+
if (error instanceof ConcurrencyError) {
|
|
236
|
+
console.log('Conflict, re-read and retry');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
134
241
|
## CLI Usage
|
|
135
242
|
|
|
136
243
|
`gh-as-db` comes with a CLI tool to help you manage your repository.
|
|
@@ -159,6 +266,7 @@ For small projects, side-projects, or internal tools, setting up a database serv
|
|
|
159
266
|
- **Write-Through**: Updates the local cache immediately after a write, preventing 404s during redirects.
|
|
160
267
|
- **Indexing**: Automatic in-memory indexing on all fields makes querying fast even as data grows.
|
|
161
268
|
- **Optimistic Concurrency**: Uses Git SHAs to ensure that you don't overwrite changes made by another client.
|
|
269
|
+
- **Automatic Retries**: Transient failures and rate limits are handled transparently with exponential backoff.
|
|
162
270
|
|
|
163
271
|
## License
|
|
164
272
|
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
export interface RetryConfig {
|
|
2
|
+
maxRetries?: number;
|
|
3
|
+
baseDelay?: number;
|
|
4
|
+
maxDelay?: number;
|
|
5
|
+
}
|
|
1
6
|
export interface GitHubDBConfig {
|
|
2
7
|
accessToken: string;
|
|
3
8
|
owner: string;
|
|
4
9
|
repo: string;
|
|
10
|
+
branch?: string;
|
|
5
11
|
cacheTTL?: number;
|
|
12
|
+
retry?: RetryConfig | false;
|
|
6
13
|
}
|
|
7
14
|
export type Schema = Record<string, any>;
|
|
8
15
|
export type MiddlewareOperation = "create" | "update" | "read" | "delete";
|
|
@@ -38,13 +45,32 @@ export declare class ConcurrencyError extends Error {
|
|
|
38
45
|
readonly path: string;
|
|
39
46
|
constructor(path: string);
|
|
40
47
|
}
|
|
48
|
+
export declare class RateLimitError extends Error {
|
|
49
|
+
readonly retryAfter?: number | undefined;
|
|
50
|
+
constructor(retryAfter?: number | undefined);
|
|
51
|
+
}
|
|
41
52
|
export interface StorageResponse<T> {
|
|
42
53
|
data: T;
|
|
43
54
|
sha: string;
|
|
44
55
|
}
|
|
56
|
+
export interface Validator<T> {
|
|
57
|
+
validate: (data: unknown) => Promise<T> | T;
|
|
58
|
+
}
|
|
59
|
+
export interface CommitChange {
|
|
60
|
+
path: string;
|
|
61
|
+
content: any;
|
|
62
|
+
}
|
|
63
|
+
export type StorageStrategy = "single-file" | "sharded";
|
|
45
64
|
export interface IStorageProvider {
|
|
46
65
|
testConnection(): Promise<boolean>;
|
|
47
66
|
exists(path: string): Promise<boolean>;
|
|
48
67
|
readJson<T>(path: string): Promise<StorageResponse<T>>;
|
|
49
68
|
writeJson<T>(path: string, content: T, message: string, sha?: string): Promise<string>;
|
|
69
|
+
deleteFile(path: string, message: string, sha: string): Promise<void>;
|
|
70
|
+
listDirectory(path: string): Promise<{
|
|
71
|
+
path: string;
|
|
72
|
+
sha: string;
|
|
73
|
+
type: "file" | "dir";
|
|
74
|
+
}[]>;
|
|
75
|
+
commit(changes: CommitChange[], message: string): Promise<string>;
|
|
50
76
|
}
|
package/dist/core/types.js
CHANGED
|
@@ -6,3 +6,11 @@ export class ConcurrencyError extends Error {
|
|
|
6
6
|
this.name = "ConcurrencyError";
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
export class RateLimitError extends Error {
|
|
10
|
+
retryAfter;
|
|
11
|
+
constructor(retryAfter) {
|
|
12
|
+
super(`GitHub API rate limit exceeded${retryAfter ? `. Retry after ${retryAfter}s` : ""}`);
|
|
13
|
+
this.retryAfter = retryAfter;
|
|
14
|
+
this.name = "RateLimitError";
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GitHubDBConfig, IStorageProvider, StorageResponse } from "../core/types.js";
|
|
1
|
+
import { GitHubDBConfig, IStorageProvider, StorageResponse, CommitChange } from "../core/types.js";
|
|
2
2
|
import { ICacheProvider } from "./cache-provider.js";
|
|
3
3
|
export declare class GitHubStorageProvider implements IStorageProvider {
|
|
4
4
|
private config;
|
|
@@ -7,8 +7,16 @@ export declare class GitHubStorageProvider implements IStorageProvider {
|
|
|
7
7
|
private staleCache;
|
|
8
8
|
private readonly DEFAULT_TTL;
|
|
9
9
|
constructor(config: GitHubDBConfig, cache?: ICacheProvider);
|
|
10
|
+
private retryWithBackoff;
|
|
10
11
|
testConnection(): Promise<boolean>;
|
|
11
12
|
exists(path: string): Promise<boolean>;
|
|
12
13
|
readJson<T>(path: string): Promise<StorageResponse<T>>;
|
|
13
14
|
writeJson<T>(path: string, content: T, message: string, sha?: string): Promise<string>;
|
|
15
|
+
commit(changes: CommitChange[], message: string): Promise<string>;
|
|
16
|
+
deleteFile(path: string, message: string, sha: string): Promise<void>;
|
|
17
|
+
listDirectory(path: string): Promise<{
|
|
18
|
+
path: string;
|
|
19
|
+
sha: string;
|
|
20
|
+
type: "file" | "dir";
|
|
21
|
+
}[]>;
|
|
14
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Octokit } from "@octokit/rest";
|
|
2
|
-
import { ConcurrencyError, } from "../core/types.js";
|
|
2
|
+
import { ConcurrencyError, RateLimitError, } from "../core/types.js";
|
|
3
3
|
import { MemoryCacheProvider } from "./cache-provider.js";
|
|
4
4
|
export class GitHubStorageProvider {
|
|
5
5
|
config;
|
|
@@ -14,12 +14,58 @@ export class GitHubStorageProvider {
|
|
|
14
14
|
});
|
|
15
15
|
this.cache = cache || new MemoryCacheProvider();
|
|
16
16
|
}
|
|
17
|
+
async retryWithBackoff(fn) {
|
|
18
|
+
if (this.config.retry === false)
|
|
19
|
+
return fn();
|
|
20
|
+
const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000, } = this.config.retry ?? {};
|
|
21
|
+
let lastError;
|
|
22
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
const status = error?.status;
|
|
29
|
+
// Non-retryable: 409 (concurrency), 4xx client errors (except 429)
|
|
30
|
+
if (status === 409)
|
|
31
|
+
throw error;
|
|
32
|
+
if (status && status < 500 && status !== 429)
|
|
33
|
+
throw error;
|
|
34
|
+
// Errors without a status code (non-HTTP errors) โ rethrow
|
|
35
|
+
if (!status)
|
|
36
|
+
throw error;
|
|
37
|
+
// Last attempt โ don't wait, just break
|
|
38
|
+
if (attempt === maxRetries)
|
|
39
|
+
break;
|
|
40
|
+
// Calculate delay
|
|
41
|
+
let delay;
|
|
42
|
+
if (status === 429 && error.response?.headers?.["retry-after"]) {
|
|
43
|
+
delay =
|
|
44
|
+
parseInt(error.response.headers["retry-after"], 10) * 1000;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
delay = baseDelay * Math.pow(2, attempt);
|
|
48
|
+
// Add jitter to avoid thundering herd on simultaneous retries
|
|
49
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
50
|
+
}
|
|
51
|
+
// Cap all delays (including Retry-After) against maxDelay
|
|
52
|
+
delay = Math.min(delay, maxDelay);
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Exhausted retries on 429 โ RateLimitError
|
|
57
|
+
if (lastError.status === 429) {
|
|
58
|
+
const retryAfter = lastError.response?.headers?.["retry-after"];
|
|
59
|
+
throw new RateLimitError(retryAfter ? parseInt(retryAfter, 10) : undefined);
|
|
60
|
+
}
|
|
61
|
+
throw lastError;
|
|
62
|
+
}
|
|
17
63
|
async testConnection() {
|
|
18
64
|
try {
|
|
19
|
-
await this.octokit.repos.get({
|
|
65
|
+
await this.retryWithBackoff(() => this.octokit.repos.get({
|
|
20
66
|
owner: this.config.owner,
|
|
21
67
|
repo: this.config.repo,
|
|
22
|
-
});
|
|
68
|
+
}));
|
|
23
69
|
return true;
|
|
24
70
|
}
|
|
25
71
|
catch (error) {
|
|
@@ -31,11 +77,11 @@ export class GitHubStorageProvider {
|
|
|
31
77
|
return true;
|
|
32
78
|
}
|
|
33
79
|
try {
|
|
34
|
-
await this.octokit.repos.getContent({
|
|
80
|
+
await this.retryWithBackoff(() => this.octokit.repos.getContent({
|
|
35
81
|
owner: this.config.owner,
|
|
36
82
|
repo: this.config.repo,
|
|
37
83
|
path,
|
|
38
|
-
});
|
|
84
|
+
}));
|
|
39
85
|
return true;
|
|
40
86
|
}
|
|
41
87
|
catch (error) {
|
|
@@ -54,19 +100,25 @@ export class GitHubStorageProvider {
|
|
|
54
100
|
// 2. Check stale cache for conditional request
|
|
55
101
|
const stale = this.staleCache.get(path);
|
|
56
102
|
try {
|
|
57
|
-
const response = await this.octokit.repos.getContent({
|
|
103
|
+
const response = await this.retryWithBackoff(() => this.octokit.repos.getContent({
|
|
58
104
|
owner: this.config.owner,
|
|
59
105
|
repo: this.config.repo,
|
|
60
106
|
path,
|
|
61
107
|
headers: stale ? { "if-none-match": `"${stale.sha}"` } : {},
|
|
62
|
-
});
|
|
108
|
+
}));
|
|
63
109
|
if (Array.isArray(response.data)) {
|
|
64
110
|
throw new Error("Path is a directory, not a file");
|
|
65
111
|
}
|
|
66
112
|
if (!("content" in response.data) || !("sha" in response.data)) {
|
|
67
113
|
throw new Error("No content or SHA in response");
|
|
68
114
|
}
|
|
69
|
-
const
|
|
115
|
+
const binary = atob(response.data.content.replace(/\s/g, ""));
|
|
116
|
+
const bytes = new Uint8Array(binary.length);
|
|
117
|
+
for (let i = 0; i < binary.length; i++) {
|
|
118
|
+
bytes[i] = binary.charCodeAt(i);
|
|
119
|
+
}
|
|
120
|
+
const decoder = new TextDecoder("utf-8");
|
|
121
|
+
const content = decoder.decode(bytes);
|
|
70
122
|
const result = {
|
|
71
123
|
data: JSON.parse(content),
|
|
72
124
|
sha: response.data.sha,
|
|
@@ -96,11 +148,11 @@ export class GitHubStorageProvider {
|
|
|
96
148
|
}
|
|
97
149
|
else {
|
|
98
150
|
try {
|
|
99
|
-
const existing = await this.octokit.repos.getContent({
|
|
151
|
+
const existing = await this.retryWithBackoff(() => this.octokit.repos.getContent({
|
|
100
152
|
owner: this.config.owner,
|
|
101
153
|
repo: this.config.repo,
|
|
102
154
|
path,
|
|
103
|
-
});
|
|
155
|
+
}));
|
|
104
156
|
if (!Array.isArray(existing.data) && "sha" in existing.data) {
|
|
105
157
|
internalSha = existing.data.sha;
|
|
106
158
|
}
|
|
@@ -112,15 +164,20 @@ export class GitHubStorageProvider {
|
|
|
112
164
|
}
|
|
113
165
|
}
|
|
114
166
|
}
|
|
167
|
+
// Note: internalSha is resolved before the retry wrapper. If a write fails
|
|
168
|
+
// with a 5xx after GitHub has already committed the file (e.g. network timeout),
|
|
169
|
+
// the retry will reuse the stale sha and may fail with a 409.
|
|
115
170
|
try {
|
|
116
|
-
const response = await this.octokit.repos.createOrUpdateFileContents({
|
|
171
|
+
const response = await this.retryWithBackoff(() => this.octokit.repos.createOrUpdateFileContents({
|
|
117
172
|
owner: this.config.owner,
|
|
118
173
|
repo: this.config.repo,
|
|
119
174
|
path,
|
|
120
175
|
message,
|
|
121
|
-
content:
|
|
176
|
+
content: btoa(Array.from(new TextEncoder().encode(JSON.stringify(content, null, 2)))
|
|
177
|
+
.map((b) => String.fromCharCode(b))
|
|
178
|
+
.join("")),
|
|
122
179
|
sha: internalSha,
|
|
123
|
-
});
|
|
180
|
+
}));
|
|
124
181
|
const newSha = response.data.content?.sha || "";
|
|
125
182
|
const result = { data: content, sha: newSha };
|
|
126
183
|
const ttl = this.config.cacheTTL ?? this.DEFAULT_TTL;
|
|
@@ -135,4 +192,117 @@ export class GitHubStorageProvider {
|
|
|
135
192
|
throw error;
|
|
136
193
|
}
|
|
137
194
|
}
|
|
195
|
+
async commit(changes, message) {
|
|
196
|
+
if (changes.length === 0) {
|
|
197
|
+
throw new Error("No changes to commit");
|
|
198
|
+
}
|
|
199
|
+
const branch = this.config.branch || "main";
|
|
200
|
+
try {
|
|
201
|
+
// 1. Get the current head SHA
|
|
202
|
+
const { data: ref } = await this.retryWithBackoff(() => this.octokit.git.getRef({
|
|
203
|
+
owner: this.config.owner,
|
|
204
|
+
repo: this.config.repo,
|
|
205
|
+
ref: `heads/${branch}`,
|
|
206
|
+
}));
|
|
207
|
+
const latestCommitSha = ref.object.sha;
|
|
208
|
+
// 2. Get the tree SHA of the latest commit
|
|
209
|
+
const { data: latestCommit } = await this.retryWithBackoff(() => this.octokit.git.getCommit({
|
|
210
|
+
owner: this.config.owner,
|
|
211
|
+
repo: this.config.repo,
|
|
212
|
+
commit_sha: latestCommitSha,
|
|
213
|
+
}));
|
|
214
|
+
const baseTreeSha = latestCommit.tree.sha;
|
|
215
|
+
// 3. Create a new tree with multiple files
|
|
216
|
+
const treeEntries = changes.map((change) => {
|
|
217
|
+
if (change.content === null) {
|
|
218
|
+
return {
|
|
219
|
+
path: change.path,
|
|
220
|
+
mode: "100644",
|
|
221
|
+
type: "blob",
|
|
222
|
+
sha: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
path: change.path,
|
|
227
|
+
mode: "100644",
|
|
228
|
+
type: "blob",
|
|
229
|
+
content: JSON.stringify(change.content, null, 2),
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
const { data: newTree } = await this.retryWithBackoff(() => this.octokit.git.createTree({
|
|
233
|
+
owner: this.config.owner,
|
|
234
|
+
repo: this.config.repo,
|
|
235
|
+
base_tree: baseTreeSha,
|
|
236
|
+
tree: treeEntries,
|
|
237
|
+
}));
|
|
238
|
+
// 4. Create a new commit
|
|
239
|
+
const { data: newCommit } = await this.retryWithBackoff(() => this.octokit.git.createCommit({
|
|
240
|
+
owner: this.config.owner,
|
|
241
|
+
repo: this.config.repo,
|
|
242
|
+
message,
|
|
243
|
+
tree: newTree.sha,
|
|
244
|
+
parents: [latestCommitSha],
|
|
245
|
+
}));
|
|
246
|
+
// 5. Update the reference
|
|
247
|
+
await this.retryWithBackoff(() => this.octokit.git.updateRef({
|
|
248
|
+
owner: this.config.owner,
|
|
249
|
+
repo: this.config.repo,
|
|
250
|
+
ref: `heads/${branch}`,
|
|
251
|
+
sha: newCommit.sha,
|
|
252
|
+
}));
|
|
253
|
+
// 6. Update cache for all involved files
|
|
254
|
+
const ttl = this.config.cacheTTL ?? this.DEFAULT_TTL;
|
|
255
|
+
for (const entry of newTree.tree) {
|
|
256
|
+
if (entry.path && entry.sha) {
|
|
257
|
+
const change = changes.find((c) => c.path === entry.path);
|
|
258
|
+
if (change) {
|
|
259
|
+
const result = { data: change.content, sha: entry.sha };
|
|
260
|
+
this.cache.set(entry.path, result, ttl);
|
|
261
|
+
this.staleCache.set(entry.path, result);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return newCommit.sha;
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error.status === 409) {
|
|
269
|
+
throw new ConcurrencyError("batch-commit");
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async deleteFile(path, message, sha) {
|
|
275
|
+
await this.retryWithBackoff(() => this.octokit.repos.deleteFile({
|
|
276
|
+
owner: this.config.owner,
|
|
277
|
+
repo: this.config.repo,
|
|
278
|
+
path,
|
|
279
|
+
message,
|
|
280
|
+
sha,
|
|
281
|
+
}));
|
|
282
|
+
this.cache.delete(path);
|
|
283
|
+
this.staleCache.delete(path);
|
|
284
|
+
}
|
|
285
|
+
async listDirectory(path) {
|
|
286
|
+
try {
|
|
287
|
+
const response = await this.retryWithBackoff(() => this.octokit.repos.getContent({
|
|
288
|
+
owner: this.config.owner,
|
|
289
|
+
repo: this.config.repo,
|
|
290
|
+
path,
|
|
291
|
+
}));
|
|
292
|
+
if (!Array.isArray(response.data)) {
|
|
293
|
+
throw new Error("Path is not a directory");
|
|
294
|
+
}
|
|
295
|
+
return response.data.map((item) => ({
|
|
296
|
+
path: item.path,
|
|
297
|
+
sha: item.sha,
|
|
298
|
+
type: item.type,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error.status === 404) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
138
308
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CommitChange, IStorageProvider, StorageResponse } from "../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A decorator for IStorageProvider that buffers writes in memory.
|
|
4
|
+
* Used during transactions to group multiple operations into a single commit.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TransactionStorageProvider implements IStorageProvider {
|
|
7
|
+
private readonly baseStorage;
|
|
8
|
+
private pendingChanges;
|
|
9
|
+
constructor(baseStorage: IStorageProvider);
|
|
10
|
+
testConnection(): Promise<boolean>;
|
|
11
|
+
exists(path: string): Promise<boolean>;
|
|
12
|
+
readJson<T>(path: string): Promise<StorageResponse<T>>;
|
|
13
|
+
writeJson<T>(path: string, content: T, _message: string, _sha?: string): Promise<string>;
|
|
14
|
+
deleteFile(path: string, _message: string, _sha: string): Promise<void>;
|
|
15
|
+
listDirectory(path: string): Promise<{
|
|
16
|
+
path: string;
|
|
17
|
+
sha: string;
|
|
18
|
+
type: "file" | "dir";
|
|
19
|
+
}[]>;
|
|
20
|
+
commit(changes: CommitChange[], message: string): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Returns all pending changes in this transaction.
|
|
23
|
+
*/
|
|
24
|
+
getChanges(): CommitChange[];
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decorator for IStorageProvider that buffers writes in memory.
|
|
3
|
+
* Used during transactions to group multiple operations into a single commit.
|
|
4
|
+
*/
|
|
5
|
+
export class TransactionStorageProvider {
|
|
6
|
+
baseStorage;
|
|
7
|
+
pendingChanges = new Map();
|
|
8
|
+
constructor(baseStorage) {
|
|
9
|
+
this.baseStorage = baseStorage;
|
|
10
|
+
}
|
|
11
|
+
async testConnection() {
|
|
12
|
+
return this.baseStorage.testConnection();
|
|
13
|
+
}
|
|
14
|
+
async exists(path) {
|
|
15
|
+
if (this.pendingChanges.has(path)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return this.baseStorage.exists(path);
|
|
19
|
+
}
|
|
20
|
+
async readJson(path) {
|
|
21
|
+
const pending = this.pendingChanges.get(path);
|
|
22
|
+
if (pending !== undefined) {
|
|
23
|
+
return {
|
|
24
|
+
data: pending,
|
|
25
|
+
sha: "transaction-pending-sha",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return this.baseStorage.readJson(path);
|
|
29
|
+
}
|
|
30
|
+
async writeJson(path, content, _message, _sha) {
|
|
31
|
+
this.pendingChanges.set(path, content);
|
|
32
|
+
return "transaction-pending-sha";
|
|
33
|
+
}
|
|
34
|
+
async deleteFile(path, _message, _sha) {
|
|
35
|
+
this.pendingChanges.set(path, null);
|
|
36
|
+
}
|
|
37
|
+
async listDirectory(path) {
|
|
38
|
+
// For now, we delegate to base storage.
|
|
39
|
+
// In sharded mode, we might need to merge with pending changes.
|
|
40
|
+
return this.baseStorage.listDirectory(path);
|
|
41
|
+
}
|
|
42
|
+
async commit(changes, message) {
|
|
43
|
+
return this.baseStorage.commit(changes, message);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns all pending changes in this transaction.
|
|
47
|
+
*/
|
|
48
|
+
getChanges() {
|
|
49
|
+
return Array.from(this.pendingChanges.entries()).map(([path, content]) => ({
|
|
50
|
+
path,
|
|
51
|
+
content,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/ui/cli/index.js
CHANGED
|
File without changes
|
package/dist/ui/collection.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { IStorageProvider, Middleware, QueryOptions, Schema } from "../core/types.js";
|
|
1
|
+
import { IStorageProvider, Middleware, QueryOptions, Schema, StorageStrategy, Validator } from "../core/types.js";
|
|
2
2
|
export declare class Collection<T extends Schema> {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
private readonly storage;
|
|
5
|
-
private readonly middleware;
|
|
6
5
|
private lastSha;
|
|
7
6
|
private indexer;
|
|
8
7
|
private items;
|
|
9
8
|
private dataLoaded;
|
|
10
|
-
|
|
9
|
+
private middleware;
|
|
10
|
+
private validator?;
|
|
11
|
+
private strategy;
|
|
12
|
+
private shas;
|
|
13
|
+
constructor(name: string, storage: IStorageProvider, middlewareOrOptions?: Middleware<T>[] | {
|
|
14
|
+
middleware?: Middleware<T>[];
|
|
15
|
+
validator?: Validator<T>;
|
|
16
|
+
strategy?: StorageStrategy;
|
|
17
|
+
});
|
|
11
18
|
private get path();
|
|
19
|
+
private getItemPath;
|
|
12
20
|
create(item: T): Promise<T>;
|
|
13
21
|
find(queryOrPredicate?: ((item: T) => boolean) | QueryOptions<T>): Promise<T[]>;
|
|
14
22
|
private applyQueryOptions;
|
package/dist/ui/collection.js
CHANGED
|
@@ -3,21 +3,42 @@ import { Indexer } from "../core/indexer.js";
|
|
|
3
3
|
export class Collection {
|
|
4
4
|
name;
|
|
5
5
|
storage;
|
|
6
|
-
middleware;
|
|
7
6
|
lastSha;
|
|
8
7
|
indexer = new Indexer();
|
|
9
8
|
items = [];
|
|
10
9
|
dataLoaded = false;
|
|
11
|
-
|
|
10
|
+
middleware;
|
|
11
|
+
validator;
|
|
12
|
+
strategy;
|
|
13
|
+
shas = new Map(); // path -> sha for sharded mode
|
|
14
|
+
constructor(name, storage, middlewareOrOptions) {
|
|
12
15
|
this.name = name;
|
|
13
16
|
this.storage = storage;
|
|
14
|
-
|
|
17
|
+
if (Array.isArray(middlewareOrOptions)) {
|
|
18
|
+
this.middleware = middlewareOrOptions;
|
|
19
|
+
this.strategy = "single-file";
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.middleware = middlewareOrOptions?.middleware || [];
|
|
23
|
+
this.validator = middlewareOrOptions?.validator;
|
|
24
|
+
this.strategy = middlewareOrOptions?.strategy || "single-file";
|
|
25
|
+
}
|
|
15
26
|
}
|
|
16
27
|
get path() {
|
|
17
|
-
return `${this.name}.json`;
|
|
28
|
+
return this.strategy === "sharded" ? `${this.name}/` : `${this.name}.json`;
|
|
29
|
+
}
|
|
30
|
+
getItemPath(id) {
|
|
31
|
+
return this.strategy === "sharded"
|
|
32
|
+
? `${this.name}/${id}.json`
|
|
33
|
+
: `${this.name}.json`;
|
|
18
34
|
}
|
|
19
35
|
async create(item) {
|
|
20
36
|
let finalItem = item;
|
|
37
|
+
// 1. Validation
|
|
38
|
+
if (this.validator) {
|
|
39
|
+
finalItem = await this.validator.validate(finalItem);
|
|
40
|
+
}
|
|
41
|
+
// 2. Middleware
|
|
21
42
|
const context = {
|
|
22
43
|
collection: this.name,
|
|
23
44
|
operation: "create",
|
|
@@ -27,6 +48,22 @@ export class Collection {
|
|
|
27
48
|
finalItem = await mw.beforeSave(finalItem, context);
|
|
28
49
|
}
|
|
29
50
|
}
|
|
51
|
+
if (this.strategy === "sharded") {
|
|
52
|
+
const itemPath = this.getItemPath(finalItem.id);
|
|
53
|
+
const sha = await this.storage.writeJson(itemPath, finalItem, `Create item ${finalItem.id} in ${this.name}`, this.shas.get(itemPath));
|
|
54
|
+
this.shas.set(itemPath, sha);
|
|
55
|
+
if (this.dataLoaded) {
|
|
56
|
+
const index = this.items.findIndex((i) => i.id === finalItem.id);
|
|
57
|
+
if (index > -1) {
|
|
58
|
+
this.items[index] = finalItem;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.items.push(finalItem);
|
|
62
|
+
}
|
|
63
|
+
this.indexer.add(finalItem);
|
|
64
|
+
}
|
|
65
|
+
return finalItem;
|
|
66
|
+
}
|
|
30
67
|
let items = [];
|
|
31
68
|
if (this.dataLoaded) {
|
|
32
69
|
items = [...this.items];
|
|
@@ -92,9 +129,21 @@ export class Collection {
|
|
|
92
129
|
}
|
|
93
130
|
return this.items;
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
132
|
+
if (this.strategy === "sharded") {
|
|
133
|
+
const files = await this.storage.listDirectory(this.name);
|
|
134
|
+
items = await Promise.all(files
|
|
135
|
+
.filter((f) => f.type === "file" && f.path.endsWith(".json"))
|
|
136
|
+
.map(async (file) => {
|
|
137
|
+
const response = await this.storage.readJson(file.path);
|
|
138
|
+
this.shas.set(file.path, response.sha);
|
|
139
|
+
return response.data;
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const response = await this.storage.readJson(this.path);
|
|
144
|
+
this.lastSha = response.sha;
|
|
145
|
+
items = response.data;
|
|
146
|
+
}
|
|
98
147
|
items = await Promise.all(items.map(async (item) => {
|
|
99
148
|
let currentItem = item;
|
|
100
149
|
for (const mw of this.middleware) {
|
|
@@ -177,6 +226,31 @@ export class Collection {
|
|
|
177
226
|
const results = this.indexer.query("id", id);
|
|
178
227
|
return results && results.length > 0 ? results[0] : null;
|
|
179
228
|
}
|
|
229
|
+
if (this.strategy === "sharded") {
|
|
230
|
+
try {
|
|
231
|
+
const itemPath = this.getItemPath(id);
|
|
232
|
+
const response = await this.storage.readJson(itemPath);
|
|
233
|
+
this.shas.set(itemPath, response.sha);
|
|
234
|
+
let finalItem = response.data;
|
|
235
|
+
const context = {
|
|
236
|
+
collection: this.name,
|
|
237
|
+
operation: "read",
|
|
238
|
+
};
|
|
239
|
+
for (const mw of this.middleware) {
|
|
240
|
+
if (mw.afterRead) {
|
|
241
|
+
finalItem = await mw.afterRead(finalItem, context);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// If data is not fully loaded, we don't update this.items yet to avoid partial consistency
|
|
245
|
+
// But if someone calls find() later it will load everything.
|
|
246
|
+
return finalItem;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (error.status === 404)
|
|
250
|
+
return null;
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
180
254
|
const items = await this.find();
|
|
181
255
|
return items.find((item) => item.id === id) || null;
|
|
182
256
|
}
|
|
@@ -189,6 +263,11 @@ export class Collection {
|
|
|
189
263
|
const originalItem = items[index];
|
|
190
264
|
items[index] = { ...items[index], ...updates };
|
|
191
265
|
let finalItem = items[index];
|
|
266
|
+
// 1. Validation
|
|
267
|
+
if (this.validator) {
|
|
268
|
+
finalItem = await this.validator.validate(finalItem);
|
|
269
|
+
}
|
|
270
|
+
// 2. Middleware
|
|
192
271
|
const context = {
|
|
193
272
|
collection: this.name,
|
|
194
273
|
operation: "update",
|
|
@@ -199,14 +278,29 @@ export class Collection {
|
|
|
199
278
|
}
|
|
200
279
|
}
|
|
201
280
|
items[index] = finalItem;
|
|
202
|
-
|
|
203
|
-
|
|
281
|
+
if (this.strategy === "sharded") {
|
|
282
|
+
const itemPath = this.getItemPath(id);
|
|
283
|
+
try {
|
|
284
|
+
const sha = await this.storage.writeJson(itemPath, finalItem, `Update item ${id} in ${this.name}`, this.shas.get(itemPath));
|
|
285
|
+
this.shas.set(itemPath, sha);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof ConcurrencyError) {
|
|
289
|
+
this.dataLoaded = false;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
204
293
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.
|
|
294
|
+
else {
|
|
295
|
+
try {
|
|
296
|
+
this.lastSha = await this.storage.writeJson(this.path, items, `Update item ${id} in ${this.name}`, this.lastSha);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error instanceof ConcurrencyError) {
|
|
300
|
+
this.dataLoaded = false;
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
208
303
|
}
|
|
209
|
-
throw error;
|
|
210
304
|
}
|
|
211
305
|
if (this.dataLoaded) {
|
|
212
306
|
this.items = items;
|
|
@@ -222,14 +316,29 @@ export class Collection {
|
|
|
222
316
|
}
|
|
223
317
|
const itemToDelete = items[index];
|
|
224
318
|
const filtered = items.filter((_, i) => i !== index);
|
|
225
|
-
|
|
226
|
-
|
|
319
|
+
if (this.strategy === "sharded") {
|
|
320
|
+
const itemPath = this.getItemPath(id);
|
|
321
|
+
const sha = this.shas.get(itemPath);
|
|
322
|
+
if (!sha) {
|
|
323
|
+
// We need the SHA to delete. If we don't have it, we must fetch it.
|
|
324
|
+
const response = await this.storage.readJson(itemPath);
|
|
325
|
+
await this.storage.deleteFile(itemPath, `Delete item ${id} from ${this.name}`, response.sha);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
await this.storage.deleteFile(itemPath, `Delete item ${id} from ${this.name}`, sha);
|
|
329
|
+
}
|
|
330
|
+
this.shas.delete(itemPath);
|
|
227
331
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
332
|
+
else {
|
|
333
|
+
try {
|
|
334
|
+
this.lastSha = await this.storage.writeJson(this.path, filtered, `Delete item ${id} from ${this.name}`, this.lastSha);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error instanceof ConcurrencyError) {
|
|
338
|
+
this.dataLoaded = false;
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
231
341
|
}
|
|
232
|
-
throw error;
|
|
233
342
|
}
|
|
234
343
|
if (this.dataLoaded) {
|
|
235
344
|
this.items = filtered;
|
package/dist/ui/github-db.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { GitHubDBConfig, IStorageProvider, Middleware, Schema } from "../core/types.js";
|
|
1
|
+
import { GitHubDBConfig, IStorageProvider, Middleware, Schema, StorageStrategy, Validator } from "../core/types.js";
|
|
2
2
|
import { Collection } from "./collection.js";
|
|
3
|
+
import { Transaction } from "./transaction.js";
|
|
3
4
|
export declare class GitHubDB {
|
|
4
5
|
readonly config: GitHubDBConfig;
|
|
5
6
|
readonly storage: IStorageProvider;
|
|
@@ -7,5 +8,8 @@ export declare class GitHubDB {
|
|
|
7
8
|
connect(): Promise<boolean>;
|
|
8
9
|
collection<T extends Schema>(name: string, options?: {
|
|
9
10
|
middleware?: Middleware<T>[];
|
|
11
|
+
validator?: Validator<T>;
|
|
12
|
+
strategy?: StorageStrategy;
|
|
10
13
|
}): Collection<T>;
|
|
14
|
+
transaction(fn: (tx: Transaction) => Promise<void>, message?: string): Promise<string>;
|
|
11
15
|
}
|
package/dist/ui/github-db.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GitHubStorageProvider } from "../infrastructure/github-storage.js";
|
|
2
2
|
import { Collection } from "./collection.js";
|
|
3
|
+
import { Transaction } from "./transaction.js";
|
|
3
4
|
export class GitHubDB {
|
|
4
5
|
config;
|
|
5
6
|
storage;
|
|
@@ -20,6 +21,11 @@ export class GitHubDB {
|
|
|
20
21
|
return this.storage.testConnection();
|
|
21
22
|
}
|
|
22
23
|
collection(name, options = {}) {
|
|
23
|
-
return new Collection(name, this.storage, options
|
|
24
|
+
return new Collection(name, this.storage, options);
|
|
25
|
+
}
|
|
26
|
+
async transaction(fn, message = "Transaction commit") {
|
|
27
|
+
const tx = new Transaction(this.storage);
|
|
28
|
+
await fn(tx);
|
|
29
|
+
return tx.commit(message);
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { IStorageProvider, Middleware, Schema, Validator } from "../core/types.js";
|
|
2
|
+
import { Collection } from "./collection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Handles atomic batch operations across multiple collections.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Transaction {
|
|
7
|
+
private readonly baseStorage;
|
|
8
|
+
private txStorage;
|
|
9
|
+
constructor(baseStorage: IStorageProvider);
|
|
10
|
+
/**
|
|
11
|
+
* Returns a collection instance bound to this transaction.
|
|
12
|
+
* Operations on this collection will be buffered until the transaction is committed.
|
|
13
|
+
*/
|
|
14
|
+
collection<T extends Schema>(name: string, options?: {
|
|
15
|
+
middleware?: Middleware<T>[];
|
|
16
|
+
validator?: Validator<T>;
|
|
17
|
+
}): Collection<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Commits all buffered changes in the transaction to GitHub in a single commit.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
commit(message: string): Promise<string>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TransactionStorageProvider } from "../infrastructure/transaction-storage.js";
|
|
2
|
+
import { Collection } from "./collection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Handles atomic batch operations across multiple collections.
|
|
5
|
+
*/
|
|
6
|
+
export class Transaction {
|
|
7
|
+
baseStorage;
|
|
8
|
+
txStorage;
|
|
9
|
+
constructor(baseStorage) {
|
|
10
|
+
this.baseStorage = baseStorage;
|
|
11
|
+
this.txStorage = new TransactionStorageProvider(baseStorage);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns a collection instance bound to this transaction.
|
|
15
|
+
* Operations on this collection will be buffered until the transaction is committed.
|
|
16
|
+
*/
|
|
17
|
+
collection(name, options = {}) {
|
|
18
|
+
return new Collection(name, this.txStorage, options);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Commits all buffered changes in the transaction to GitHub in a single commit.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
async commit(message) {
|
|
25
|
+
const changes = this.txStorage.getChanges();
|
|
26
|
+
if (changes.length === 0) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
return this.txStorage.commit(changes, message);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-as-db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Use a private GitHub repository as a database for your application.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,12 +20,6 @@
|
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"test": "vitest",
|
|
25
|
-
"test:run": "vitest run",
|
|
26
|
-
"build": "tsc",
|
|
27
|
-
"prepublishOnly": "npm run build"
|
|
28
|
-
},
|
|
29
23
|
"keywords": [
|
|
30
24
|
"github",
|
|
31
25
|
"database",
|
|
@@ -60,5 +54,10 @@
|
|
|
60
54
|
"chalk": "^5.4.1",
|
|
61
55
|
"commander": "^13.1.0",
|
|
62
56
|
"enquirer": "^2.4.1"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"test": "vitest",
|
|
60
|
+
"test:run": "vitest run",
|
|
61
|
+
"build": "tsc"
|
|
63
62
|
}
|
|
64
|
-
}
|
|
63
|
+
}
|