spectre.db 1.0.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/LICENSE +165 -0
- package/README.md +253 -0
- package/examples/advanced.js +85 -0
- package/examples/basic.js +76 -0
- package/examples/discord-bot.js +103 -0
- package/index.js +79 -0
- package/package.json +44 -0
- package/src/engine.js +605 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
SPECTRE.DB SOURCE AVAILABLE LICENSE
|
|
2
|
+
Version 1.0 — 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 ScarysMonsters (https://github.com/ScarysMonsters)
|
|
5
|
+
All rights reserved on the original work.
|
|
6
|
+
|
|
7
|
+
================================================================================
|
|
8
|
+
DEFINITIONS
|
|
9
|
+
================================================================================
|
|
10
|
+
|
|
11
|
+
"Software" refers to the spectre.db source code, documentation, examples,
|
|
12
|
+
configuration files, and any associated materials published in this repository.
|
|
13
|
+
|
|
14
|
+
"Author" refers to ScarysMonsters, the sole original creator and legal owner
|
|
15
|
+
of the Software.
|
|
16
|
+
|
|
17
|
+
"You" refers to any individual or legal entity exercising permissions granted
|
|
18
|
+
by this License.
|
|
19
|
+
|
|
20
|
+
"Derivative Work" means any work that is based on, incorporates, modifies,
|
|
21
|
+
translates, or is otherwise derived from the Software or any substantial part
|
|
22
|
+
of it, regardless of the form in which it is distributed.
|
|
23
|
+
|
|
24
|
+
"Commercial Use" means any use of the Software or a Derivative Work that is
|
|
25
|
+
primarily intended for or directed toward commercial advantage or monetary
|
|
26
|
+
compensation, including but not limited to selling, licensing, or providing
|
|
27
|
+
the Software as a service.
|
|
28
|
+
|
|
29
|
+
================================================================================
|
|
30
|
+
GRANT OF RIGHTS
|
|
31
|
+
================================================================================
|
|
32
|
+
|
|
33
|
+
Subject to the conditions and limitations set forth in this License, the Author
|
|
34
|
+
grants You a worldwide, royalty-free, non-exclusive, non-transferable license to:
|
|
35
|
+
|
|
36
|
+
1. Use the Software for personal, educational, or non-commercial purposes.
|
|
37
|
+
|
|
38
|
+
2. Copy and redistribute the Software in source or compiled form, provided
|
|
39
|
+
that You comply with the conditions in this License.
|
|
40
|
+
|
|
41
|
+
3. Modify the Software and create Derivative Works, provided that You comply
|
|
42
|
+
with the conditions in this License.
|
|
43
|
+
|
|
44
|
+
4. Integrate the Software or Derivative Works into your own projects,
|
|
45
|
+
including Discord bots, automation tools, and other applications, provided
|
|
46
|
+
that such integration complies with the conditions in this License.
|
|
47
|
+
|
|
48
|
+
================================================================================
|
|
49
|
+
CONDITIONS
|
|
50
|
+
================================================================================
|
|
51
|
+
|
|
52
|
+
Any exercise of the rights granted above must satisfy all of the following
|
|
53
|
+
conditions:
|
|
54
|
+
|
|
55
|
+
1. ATTRIBUTION REQUIRED
|
|
56
|
+
All copies, redistributions, or Derivative Works — whether in source or
|
|
57
|
+
binary form — must retain, in a clearly visible location:
|
|
58
|
+
|
|
59
|
+
a. The original copyright notice:
|
|
60
|
+
"Copyright (c) 2025 ScarysMonsters — spectre.db"
|
|
61
|
+
|
|
62
|
+
b. A reference to this License and a link to the original repository:
|
|
63
|
+
https://github.com/ScarysMonsters/spectre.db
|
|
64
|
+
|
|
65
|
+
c. A clear indication of any modifications made to the original Software,
|
|
66
|
+
with a description of what was changed and when.
|
|
67
|
+
|
|
68
|
+
Attribution must not suggest that the Author endorses You or your use of
|
|
69
|
+
the Software.
|
|
70
|
+
|
|
71
|
+
2. NO OWNERSHIP CLAIM
|
|
72
|
+
You may not represent, claim, imply, or otherwise assert — publicly or
|
|
73
|
+
privately — that You are the original author, creator, or owner of the
|
|
74
|
+
Software or any substantial portion of it. This prohibition applies
|
|
75
|
+
regardless of the extent of your modifications.
|
|
76
|
+
|
|
77
|
+
Publishing a Derivative Work under a different name does not transfer,
|
|
78
|
+
diminish, or otherwise affect the Author's intellectual property rights
|
|
79
|
+
over the original Software.
|
|
80
|
+
|
|
81
|
+
3. LICENSE PRESERVATION
|
|
82
|
+
All copies, redistributions, and Derivative Works must include a copy of
|
|
83
|
+
this License in full, without modification.
|
|
84
|
+
|
|
85
|
+
4. NO MISREPRESENTATION OF ORIGIN
|
|
86
|
+
You may not remove, obscure, alter, or replace any copyright notices,
|
|
87
|
+
license headers, authorship statements, or attribution markers present in
|
|
88
|
+
the original Software.
|
|
89
|
+
|
|
90
|
+
5. COMMERCIAL USE REQUIRES PRIOR WRITTEN CONSENT
|
|
91
|
+
Any Commercial Use of the Software or any Derivative Work requires the
|
|
92
|
+
Author's explicit, prior written authorization. To request commercial
|
|
93
|
+
authorization, contact the Author through the official repository.
|
|
94
|
+
|
|
95
|
+
Unauthorized Commercial Use constitutes a material breach of this License
|
|
96
|
+
and an infringement of the Author's intellectual property rights.
|
|
97
|
+
|
|
98
|
+
================================================================================
|
|
99
|
+
INTELLECTUAL PROPERTY AND ENFORCEMENT
|
|
100
|
+
================================================================================
|
|
101
|
+
|
|
102
|
+
The Software, including its architecture, design, logic, and original
|
|
103
|
+
expression, is the exclusive intellectual property of ScarysMonsters.
|
|
104
|
+
|
|
105
|
+
The Author reserves all rights not expressly granted in this License, including
|
|
106
|
+
but not limited to:
|
|
107
|
+
|
|
108
|
+
— The right to revoke this License for any individual or entity found in
|
|
109
|
+
breach of its conditions, effective immediately upon written notice.
|
|
110
|
+
|
|
111
|
+
— The right to issue takedown requests, DMCA notices, or equivalent
|
|
112
|
+
intellectual property enforcement actions against any person, organization,
|
|
113
|
+
platform, or registry (including but not limited to npm, GitHub, GitLab,
|
|
114
|
+
PyPI, or any package registry) distributing the Software or a Derivative
|
|
115
|
+
Work in violation of this License.
|
|
116
|
+
|
|
117
|
+
— The right to pursue civil remedies, including injunctive relief and
|
|
118
|
+
damages, under applicable copyright and intellectual property laws, in any
|
|
119
|
+
jurisdiction where such violations occur.
|
|
120
|
+
|
|
121
|
+
— The right to request the removal or delisting of any project, package,
|
|
122
|
+
repository, or publication that infringes upon the Author's rights under
|
|
123
|
+
this License or applicable law, through administrative, legal, or platform
|
|
124
|
+
enforcement mechanisms.
|
|
125
|
+
|
|
126
|
+
The Author's failure to enforce any provision of this License at any time shall
|
|
127
|
+
not be construed as a waiver of the Author's rights to enforce such provision
|
|
128
|
+
at a later time or in other circumstances.
|
|
129
|
+
|
|
130
|
+
================================================================================
|
|
131
|
+
DISCLAIMER OF WARRANTIES
|
|
132
|
+
================================================================================
|
|
133
|
+
|
|
134
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
135
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
136
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
|
137
|
+
|
|
138
|
+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
139
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
|
140
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
141
|
+
SOFTWARE.
|
|
142
|
+
|
|
143
|
+
================================================================================
|
|
144
|
+
TERMINATION
|
|
145
|
+
================================================================================
|
|
146
|
+
|
|
147
|
+
This License and the rights granted hereunder will terminate automatically and
|
|
148
|
+
immediately upon any breach by You of the conditions set forth in this License.
|
|
149
|
+
|
|
150
|
+
Upon termination, You must cease all use, distribution, and possession of the
|
|
151
|
+
Software and all Derivative Works, and destroy all copies in your control.
|
|
152
|
+
|
|
153
|
+
Termination of this License does not limit the Author's right to pursue
|
|
154
|
+
additional remedies for damages or injunctive relief arising from the breach.
|
|
155
|
+
|
|
156
|
+
================================================================================
|
|
157
|
+
CONTACT
|
|
158
|
+
================================================================================
|
|
159
|
+
|
|
160
|
+
For questions regarding this License, commercial use authorization, or to
|
|
161
|
+
report a violation:
|
|
162
|
+
|
|
163
|
+
GitHub: https://github.com/ScarysMonsters
|
|
164
|
+
Repository: https://github.com/ScarysMonsters/spectre.db
|
|
165
|
+
Issues: https://github.com/ScarysMonsters/spectre.db/issues
|
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
> [!IMPORTANT]
|
|
2
|
+
> ## Project Status
|
|
3
|
+
>
|
|
4
|
+
> **This project is actively maintained and developed by ScarysMonsters.**
|
|
5
|
+
|
|
6
|
+
> [!NOTE]
|
|
7
|
+
> **spectre.db is a zero-dependency, production-grade file-based JSON database for Node.js bots and small applications.**
|
|
8
|
+
> **Built on a WAL (Write-Ahead Log) architecture with atomic writes, LRU cache, real transactions, and optional AES-256 encryption.**
|
|
9
|
+
|
|
10
|
+
## About
|
|
11
|
+
|
|
12
|
+
<strong>Welcome to `spectre.db`, a Node.js module that provides a lightweight, persistent key-value database engineered for Discord bots and backend services.</strong>
|
|
13
|
+
|
|
14
|
+
- spectre.db is a [Node.js](https://nodejs.org) module with **zero external dependencies** — powered entirely by Node.js built-ins.
|
|
15
|
+
- Uses a **WAL + snapshot** architecture so your data is never at risk from an unclean shutdown.
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
<p>
|
|
19
|
+
<a href="https://www.npmjs.com/package/spectre.db"><img src="https://img.shields.io/npm/v/spectre.db.svg" alt="npm version" /></a>
|
|
20
|
+
<a href="https://www.npmjs.com/package/spectre.db"><img src="https://img.shields.io/npm/dt/spectre.db.svg" alt="npm downloads" /></a>
|
|
21
|
+
<a href="https://github.com/ScarysMonsters/spectre.db"><img src="https://img.shields.io/github/stars/ScarysMonsters/spectre.db?style=flat" alt="GitHub stars" /></a>
|
|
22
|
+
<a href="https://github.com/ScarysMonsters/spectre.db/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/spectre.db.svg" alt="license" /></a>
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
### <strong>[Example Code](https://github.com/ScarysMonsters/spectre.db/tree/main/examples)</strong>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- [x] Zero external dependencies (pure Node.js built-ins)
|
|
33
|
+
- [x] WAL-based persistence — never rewrites the full file on every change
|
|
34
|
+
- [x] Atomic writes — crash-safe temp file + rename pattern
|
|
35
|
+
- [x] Backup rotation — configurable generations (`.1.bak`, `.2.bak`, ...)
|
|
36
|
+
- [x] O(1) LRU cache with prefix-index invalidation
|
|
37
|
+
- [x] Real transactions — function-based with automatic rollback on error
|
|
38
|
+
- [x] Legacy array transaction API — fully backward compatible
|
|
39
|
+
- [x] AES-256-GCM encryption — auto-applied to sensitive keys (`token`, `password`, `secret`, ...)
|
|
40
|
+
- [x] Table abstraction — scoped key namespacing
|
|
41
|
+
- [x] Dot-notation keys — `users.123.coins` works out of the box
|
|
42
|
+
- [x] Prototype pollution prevention by design
|
|
43
|
+
- [x] Events: `change`, `save`, `rollback`, `restore`, `clear`
|
|
44
|
+
- [ ] TypeScript types (planned)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
> [!NOTE]
|
|
51
|
+
> **Node.js 18.0.0 or newer is required**
|
|
52
|
+
|
|
53
|
+
```sh-session
|
|
54
|
+
npm install spectre.db@latest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const { Database } = require('spectre.db');
|
|
63
|
+
|
|
64
|
+
const db = new Database('./data/mydb', {
|
|
65
|
+
cache: true,
|
|
66
|
+
autoSave: 5000,
|
|
67
|
+
backup: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
db.ready.then(() => {
|
|
71
|
+
db.set('users.1.name', 'Alice');
|
|
72
|
+
console.log(db.get('users.1.name')); // 'Alice'
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Discord Bot Example
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const { Client, GatewayIntentBits } = require('discord.js');
|
|
82
|
+
const { Database } = require('spectre.db');
|
|
83
|
+
|
|
84
|
+
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
|
85
|
+
|
|
86
|
+
const db = new Database('./src/data/database', {
|
|
87
|
+
cache: true,
|
|
88
|
+
autoSave: 5000,
|
|
89
|
+
backup: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
client.db = db;
|
|
93
|
+
|
|
94
|
+
db.ready.then(() => client.login(process.env.TOKEN));
|
|
95
|
+
|
|
96
|
+
client.once('ready', () => {
|
|
97
|
+
console.log(`${client.user.tag} is ready!`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
process.on('SIGINT', async () => {
|
|
101
|
+
await client.db.close();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## API Reference
|
|
109
|
+
|
|
110
|
+
### Constructor
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
new Database(path, options?)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `cache` | boolean | `true` | Enable LRU cache |
|
|
119
|
+
| `maxCacheSize` | number | `1000` | Max cached entries |
|
|
120
|
+
| `cacheTTL` | number | `0` | TTL in ms (0 = forever) |
|
|
121
|
+
| `autoSave` | number | `5000` | Compaction interval in ms |
|
|
122
|
+
| `backup` | boolean | `true` | Enable backup rotation |
|
|
123
|
+
| `backupCount` | number | `3` | Number of backup generations |
|
|
124
|
+
| `compress` | boolean | `false` | Gzip snapshot compression |
|
|
125
|
+
| `encryptionKey` | string/Buffer | `null` | AES-256-GCM key for sensitive values |
|
|
126
|
+
| `warmKeys` | string[] | `[]` | Keys to pre-load into cache on startup |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Core Methods
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
db.get('users.1.name') // → value or null
|
|
134
|
+
db.set('users.1.name', 'Alice') // → value
|
|
135
|
+
db.delete('users.1.name') // → true / false
|
|
136
|
+
db.has('users.1.name') // → true / false
|
|
137
|
+
db.add('users.1.coins', 100) // → new value
|
|
138
|
+
db.sub('users.1.coins', 50) // → new value
|
|
139
|
+
db.push('users.1.roles', 'admin') // → new array length
|
|
140
|
+
db.pull('users.1.roles', 'member') // → true / false
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Query Methods
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
db.all() // → [{ ID, data }]
|
|
147
|
+
db.startsWith('users.') // → [{ ID, data }]
|
|
148
|
+
db.filter((data, id) => id.endsWith('.coins')) // → [{ ID, data }]
|
|
149
|
+
db.find((data, id) => id === 'users.1.name') // → { ID, data } | null
|
|
150
|
+
db.paginate('users.', page, limit, sortBy, sortDesc) // → { data, pagination }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Transactions
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
// Function-based (recommended)
|
|
157
|
+
await db.transaction(async (tx) => {
|
|
158
|
+
const coins = tx.get('users.1.coins') ?? 0;
|
|
159
|
+
tx.set('users.1.coins', coins - 50);
|
|
160
|
+
tx.set('users.2.coins', (tx.get('users.2.coins') ?? 0) + 50);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Array-based (legacy — fully supported)
|
|
164
|
+
await db.transaction([
|
|
165
|
+
{ type: 'set', key: 'config.debug', value: true },
|
|
166
|
+
{ type: 'add', key: 'stats.logins', value: 1 },
|
|
167
|
+
{ type: 'delete', key: 'cache.tmp' },
|
|
168
|
+
]);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Tables
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
const users = db.table('users');
|
|
175
|
+
|
|
176
|
+
users.set('1.name', 'Alice') // stored as "users.1.name"
|
|
177
|
+
users.get('1.name') // 'Alice'
|
|
178
|
+
users.count() // number of entries
|
|
179
|
+
await users.clear() // removes all users.*
|
|
180
|
+
|
|
181
|
+
await users.transaction(async (tx) => {
|
|
182
|
+
tx.add('1.coins', 100);
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Persistence & Lifecycle
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
await db.save() // force compaction now
|
|
190
|
+
await db.close() // flush + compact + release (always call on shutdown)
|
|
191
|
+
db.getStats() // { entries, cacheSize, fileSize, walOps, ... }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## How it works
|
|
197
|
+
|
|
198
|
+
### Files on disk
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
data/
|
|
202
|
+
├── mydb.snapshot ← compacted JSON state
|
|
203
|
+
├── mydb.wal ← append-only operation log
|
|
204
|
+
├── mydb.snapshot.1.bak ← most recent backup
|
|
205
|
+
├── mydb.snapshot.2.bak
|
|
206
|
+
└── mydb.snapshot.3.bak
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Write flow
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
db.set('x', 1)
|
|
213
|
+
└─ updates in-memory store immediately (synchronous)
|
|
214
|
+
└─ WAL append queued (async, non-blocking)
|
|
215
|
+
|
|
216
|
+
Every compactInterval:
|
|
217
|
+
└─ if walOps >= compactThreshold:
|
|
218
|
+
└─ serialize → unique temp file → atomic rename → truncate WAL
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Startup flow
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
new Database(path)
|
|
225
|
+
└─ load .snapshot
|
|
226
|
+
└─ replay .wal on top of snapshot
|
|
227
|
+
└─ open WAL for appending
|
|
228
|
+
└─ db.ready resolves
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Contributing
|
|
234
|
+
|
|
235
|
+
- Before creating an issue, please ensure that it hasn't already been reported/suggested.
|
|
236
|
+
- See [the contribution guide](https://github.com/ScarysMonsters/spectre.db/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
|
237
|
+
|
|
238
|
+
## Need help?
|
|
239
|
+
|
|
240
|
+
GitHub Issues: [Here](https://github.com/ScarysMonsters/spectre.db/issues)
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Other project(s)
|
|
245
|
+
|
|
246
|
+
- 🤖 [***ScarysMonsters***](https://github.com/ScarysMonsters) <br/>
|
|
247
|
+
More tools and bots.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Star History
|
|
252
|
+
|
|
253
|
+
[](https://star-history.com/#ScarysMonsters/spectre.db&Date)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Database } = require('spectre.db');
|
|
4
|
+
|
|
5
|
+
async function encryptionExample() {
|
|
6
|
+
console.log('--- Encryption ---');
|
|
7
|
+
|
|
8
|
+
const db = new Database('./data/secure', {
|
|
9
|
+
encryptionKey: 'my-super-secret-passphrase',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
await db.ready;
|
|
13
|
+
|
|
14
|
+
db.set('user.token', 'discord-bot-token-abc123');
|
|
15
|
+
db.set('user.password', 'hunter2');
|
|
16
|
+
db.set('user.name', 'Alice');
|
|
17
|
+
|
|
18
|
+
console.log('token (decrypted via get):', db.get('user.token'));
|
|
19
|
+
console.log('name (plaintext):', db.get('user.name'));
|
|
20
|
+
|
|
21
|
+
await db.close();
|
|
22
|
+
console.log('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function tableExample() {
|
|
26
|
+
console.log('--- Tables ---');
|
|
27
|
+
|
|
28
|
+
const db = new Database('./data/tables');
|
|
29
|
+
await db.ready;
|
|
30
|
+
|
|
31
|
+
const guilds = db.table('guilds');
|
|
32
|
+
const users = db.table('users');
|
|
33
|
+
|
|
34
|
+
guilds.set('123.prefix', '!');
|
|
35
|
+
guilds.set('123.lang', 'en');
|
|
36
|
+
guilds.set('456.prefix', '?');
|
|
37
|
+
|
|
38
|
+
users.set('99.xp', 0);
|
|
39
|
+
users.set('99.level', 1);
|
|
40
|
+
|
|
41
|
+
await guilds.transaction(async (tx) => {
|
|
42
|
+
tx.set('123.prefix', '$');
|
|
43
|
+
tx.set('123.modRole', 'moderator');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log('guild prefix:', guilds.get('123.prefix'));
|
|
47
|
+
console.log('guild count:', guilds.count());
|
|
48
|
+
console.log('user xp:', users.get('99.xp'));
|
|
49
|
+
|
|
50
|
+
await db.close();
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function compactionExample() {
|
|
55
|
+
console.log('--- Compaction ---');
|
|
56
|
+
|
|
57
|
+
const db = new Database('./data/compact', {
|
|
58
|
+
compactThreshold: 5,
|
|
59
|
+
compactInterval: 1000,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await db.ready;
|
|
63
|
+
|
|
64
|
+
db.on('save', (stats) => {
|
|
65
|
+
console.log('compacted — walOps reset, fileSize:', stats.fileSize, 'bytes');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < 10; i++) {
|
|
69
|
+
db.set(`counter.${i}`, i * 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await db.save();
|
|
73
|
+
|
|
74
|
+
console.log('stats after manual save:', db.getStats());
|
|
75
|
+
await db.close();
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
await encryptionExample();
|
|
81
|
+
await tableExample();
|
|
82
|
+
await compactionExample();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Database } = require('spectre.db');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const db = new Database('./data/mydb', {
|
|
7
|
+
cache: true,
|
|
8
|
+
autoSave: 5000,
|
|
9
|
+
backup: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
await db.ready;
|
|
13
|
+
|
|
14
|
+
db.set('users.1.name', 'Alice');
|
|
15
|
+
db.set('users.1.coins', 0);
|
|
16
|
+
db.set('users.1.roles', ['member']);
|
|
17
|
+
|
|
18
|
+
console.log(db.get('users.1.name'));
|
|
19
|
+
console.log(db.has('users.1.coins'));
|
|
20
|
+
|
|
21
|
+
db.add('users.1.coins', 150);
|
|
22
|
+
db.sub('users.1.coins', 50);
|
|
23
|
+
console.log('coins:', db.get('users.1.coins'));
|
|
24
|
+
|
|
25
|
+
db.push('users.1.roles', 'admin');
|
|
26
|
+
console.log('roles:', db.get('users.1.roles'));
|
|
27
|
+
|
|
28
|
+
db.pull('users.1.roles', 'member');
|
|
29
|
+
console.log('roles after pull:', db.get('users.1.roles'));
|
|
30
|
+
|
|
31
|
+
db.set('users.2.name', 'Bob');
|
|
32
|
+
db.set('users.2.coins', 500);
|
|
33
|
+
|
|
34
|
+
const all = db.all();
|
|
35
|
+
console.log('all entries:', all.length);
|
|
36
|
+
|
|
37
|
+
const rich = db.filter((data, id) => id.endsWith('.coins') && data > 100);
|
|
38
|
+
console.log('rich users:', rich);
|
|
39
|
+
|
|
40
|
+
const { data, pagination } = db.paginate('users.', 1, 10, 'data', true);
|
|
41
|
+
console.log('page:', data.length, 'total:', pagination.total);
|
|
42
|
+
|
|
43
|
+
await db.transaction(async (tx) => {
|
|
44
|
+
tx.set('users.3.name', 'Charlie');
|
|
45
|
+
tx.set('users.3.coins', tx.get('users.1.coins') + 10);
|
|
46
|
+
});
|
|
47
|
+
console.log('tx result:', db.get('users.3.name'), db.get('users.3.coins'));
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await db.transaction(async (tx) => {
|
|
51
|
+
tx.set('users.4.name', 'Dave');
|
|
52
|
+
throw new Error('Something went wrong');
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
console.log('rollback ok — users.4 exists:', db.has('users.4.name'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const users = db.table('users');
|
|
59
|
+
console.log('table count:', users.count());
|
|
60
|
+
console.log('table get 1.name:', users.get('1.name'));
|
|
61
|
+
|
|
62
|
+
await db.transaction([
|
|
63
|
+
{ type: 'set', key: 'config.debug', value: true },
|
|
64
|
+
{ type: 'set', key: 'config.version', value: 2 },
|
|
65
|
+
{ type: 'delete', key: 'users.3.name' },
|
|
66
|
+
]);
|
|
67
|
+
console.log('legacy tx config.debug:', db.get('config.debug'));
|
|
68
|
+
console.log('legacy tx users.3.name (deleted):', db.get('users.3.name'));
|
|
69
|
+
|
|
70
|
+
console.log('stats:', db.getStats());
|
|
71
|
+
|
|
72
|
+
await db.close();
|
|
73
|
+
console.log('closed.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Client, GatewayIntentBits } = require('discord.js');
|
|
4
|
+
const { Database } = require('spectre.db');
|
|
5
|
+
|
|
6
|
+
const client = new Client({
|
|
7
|
+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const db = new Database('./src/data/database', {
|
|
11
|
+
cache: true,
|
|
12
|
+
autoSave: 5000,
|
|
13
|
+
backup: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
client.db = db;
|
|
17
|
+
|
|
18
|
+
db.ready.then(() => {
|
|
19
|
+
console.log('[DB] Ready');
|
|
20
|
+
client.login(process.env.TOKEN);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
client.once('ready', () => {
|
|
24
|
+
console.log(`[Bot] Logged in as ${client.user.tag}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client.on('messageCreate', async (message) => {
|
|
28
|
+
if (message.author.bot) return;
|
|
29
|
+
|
|
30
|
+
const prefix = '!';
|
|
31
|
+
if (!message.content.startsWith(prefix)) return;
|
|
32
|
+
|
|
33
|
+
const args = message.content.slice(prefix.length).trim().split(/\s+/);
|
|
34
|
+
const command = args.shift().toLowerCase();
|
|
35
|
+
|
|
36
|
+
if (command === 'coins') {
|
|
37
|
+
const userId = message.author.id;
|
|
38
|
+
const coins = client.db.get(`users.${userId}.coins`) ?? 0;
|
|
39
|
+
return message.reply(`You have **${coins}** coins.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command === 'daily') {
|
|
43
|
+
const userId = message.author.id;
|
|
44
|
+
const lastKey = `users.${userId}.lastDaily`;
|
|
45
|
+
const last = client.db.get(lastKey) ?? 0;
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const cooldown = 24 * 60 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
if (now - last < cooldown) {
|
|
50
|
+
const remaining = Math.ceil((cooldown - (now - last)) / 3600000);
|
|
51
|
+
return message.reply(`Come back in ${remaining}h for your daily coins.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await client.db.transaction(async (tx) => {
|
|
55
|
+
const current = tx.get(`users.${userId}.coins`) ?? 0;
|
|
56
|
+
tx.set(`users.${userId}.coins`, current + 100);
|
|
57
|
+
tx.set(`users.${userId}.lastDaily`, now);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return message.reply('You claimed your **100** daily coins!');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === 'give') {
|
|
64
|
+
const target = message.mentions.users.first();
|
|
65
|
+
const amount = parseInt(args[1], 10);
|
|
66
|
+
|
|
67
|
+
if (!target || isNaN(amount) || amount <= 0) {
|
|
68
|
+
return message.reply('Usage: `!give @user <amount>`');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const senderId = message.author.id;
|
|
72
|
+
const balance = client.db.get(`users.${senderId}.coins`) ?? 0;
|
|
73
|
+
|
|
74
|
+
if (balance < amount) {
|
|
75
|
+
return message.reply(`You only have **${balance}** coins.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await client.db.transaction(async (tx) => {
|
|
79
|
+
const senderCoins = tx.get(`users.${senderId}.coins`) ?? 0;
|
|
80
|
+
const receiverCoins = tx.get(`users.${target.id}.coins`) ?? 0;
|
|
81
|
+
tx.set(`users.${senderId}.coins`, senderCoins - amount);
|
|
82
|
+
tx.set(`users.${target.id}.coins`, receiverCoins + amount);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return message.reply(`Sent **${amount}** coins to ${target.username}.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (command === 'leaderboard') {
|
|
89
|
+
const { data } = client.db.paginate('users.', 1, 10, 'data', true);
|
|
90
|
+
const filtered = data.filter(({ ID }) => ID.endsWith('.coins'));
|
|
91
|
+
const lines = filtered.map(({ ID, data: coins }, i) => {
|
|
92
|
+
const uid = ID.split('.')[1];
|
|
93
|
+
return `${i + 1}. <@${uid}> — ${coins} coins`;
|
|
94
|
+
});
|
|
95
|
+
return message.reply(lines.length ? lines.join('\n') : 'No data yet.');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
process.on('SIGINT', async () => {
|
|
100
|
+
console.log('[Bot] Shutting down...');
|
|
101
|
+
await client.db.close();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
});
|