js-doc-storeDocument database en vanilla JS — zero dependencias. Corre en Node.js, browser, Cloudflare Workers, Deno, Bun.
Queries estilo MongoDB con indices, joins, aggregation, encriptacion y autenticacion. Un solo archivo.
Instalacioncp js-doc-store.js tu-proyecto/const {
DocStore,
MemoryStorageAdapter,
FileStorageAdapter,
CloudflareKVAdapter,
EncryptedAdapter,
FieldCrypto,
Auth,
} = require ( './js-doc-store' ) ; Quick Startconst db = new DocStore ( new MemoryStorageAdapter ( ) ) ;
const users = db. collection ( 'users' ) ;
users. createIndex ( 'email' , { unique : true } ) ;
users. insert ( { name : 'Alice' , email : 'alice@test.com' , age : 30 } ) ;
users. insert ( { name : 'Bob' , email : 'bob@test.com' , age : 25 } ) ;
users. find ( { age : { $gte : 18 } } ) . sort ( { age : - 1 } ) . limit ( 10 ) . toArray ( ) ;
db. flush ( ) ; CRUD Insert
const doc = users. insert ( { name : 'Alice' , age : 30 } ) ;
users. insert ( { _id : 'custom-id' , name : 'Bob' } ) ;
users. insertMany ( [ { name : 'C' } , { name : 'D' } ] ) ; Findusers. findById ( 'custom-id' ) ;
users. findOne ( { email : 'alice@test.com' } ) ;
users. find ( { age : { $gte : 18 } } )
. sort ( { age : - 1 } )
. skip ( 20 )
. limit ( 10 )
. project ( { name : 1 , age : 1 } )
. toArray ( ) ;
users. find ( { city : 'Madrid' } ) . first ( ) ;
users. find ( { active : true } ) . count ( ) ; Updateusers. update ( { email : 'x@test.com' } , { $set : { age : 31 } } ) ;
users. updateMany ( { active : false } , { $set : { archived : true } } ) ; Removeusers. remove ( { email : 'x@test.com' } ) ;
users. removeMany ( { archived : true } ) ;
users. removeById ( 'custom-id' ) ; Countusers. count ( ) ;
users. count ( { age : { $gte : 18 } } ) ; Query Operators Comparacion
Operador
Ejemplo
Descripcion
igualdad
{ name: 'Alice' }
Campo es exactamente el valor
$eq
{ age: { $eq: 30 } }
Igual (explicito)
$ne
{ status: { $ne: 'deleted' } }
No igual
$gt
{ age: { $gt: 18 } }
Mayor que
$gte
{ age: { $gte: 18 } }
Mayor o igual
$lt
{ price: { $lt: 100 } }
Menor que
$lte
{ price: { $lte: 100 } }
Menor o igual
Set
Operador
Ejemplo
Descripcion
$in
{ status: { $in: ['active', 'pending'] } }
Valor en lista
$nin
{ role: { $nin: ['banned'] } }
Valor NO en lista
Existencia y patron
Operador
Ejemplo
Descripcion
$exists
{ phone: { $exists: true } }
Campo existe
$regex
{ name: { $regex: '^Al' } }
Match regex
$contains
{ tags: { $contains: 'admin' } }
Array contiene valor
$size
{ tags: { $size: 3 } }
Array tiene N elementos
Logicos
Operador
Ejemplo
Descripcion
$and
{ $and: [{ age: { $gte: 18 } }, { active: true }] }
Todos deben cumplir
$or
{ $or: [{ city: 'Madrid' }, { city: 'Barcelona' }] }
Al menos uno cumple
$not
{ $not: { status: 'deleted' } }
Niega el filtro
Dot notationusers. find ( { 'address.city' : 'Madrid' } ) ;
users. find ( { 'profile.settings.theme' : 'dark' } ) ; Update Operators
Operador
Ejemplo
Efecto
$set
{ $set: { name: 'Alice', age: 31 } }
Setea campos
$unset
{ $unset: { tempField: 1 } }
Elimina campos
$inc
{ $inc: { visits: 1, score: -5 } }
Incrementa/decrementa
$push
{ $push: { tags: 'new-tag' } }
Agrega a array
$pull
{ $pull: { tags: 'old-tag' } }
Remueve de array
$rename
{ $rename: { oldName: 'newName' } }
Renombra campo
Indices Hash Index (igualdad O(1))users. createIndex ( 'email' , { unique : true } ) ;
users. createIndex ( 'category' ) ;
users. findOne ( { email : 'alice@test.com' } ) ; Sorted Index (rangos + ORDER BY)users. createIndex ( 'age' , { type : 'sorted' } ) ;
users. find ( { age : { $gte : 18 , $lte : 65 } } ) . toArray ( ) ; Gestionusers. dropIndex ( 'email' ) ;
users. getIndexes ( ) ; Aggregation Pipelineorders. aggregate ( )
. match ( { status : 'completed' } )
. lookup ( { from : 'users' , localField : 'userId' , foreignField : '_id' , as : 'user' , single : true } )
. group ( 'user.name' , {
total : { $sum : 'price' } ,
count : { $count : true } ,
avgPrice : { $avg : 'price' } ,
minPrice : { $min : 'price' } ,
maxPrice : { $max : 'price' } ,
} )
. sort ( { total : - 1 } )
. limit ( 10 )
. toArray ( ) ; Stages disponibles
Stage
Descripcion
.match(filter)
Filtra documentos
.lookup(opts)
Join con otra coleccion
.group(field, accumulators)
Agrupa y calcula agregados
.sort(spec)
Ordena (1 asc, -1 desc)
.limit(n)
Limita resultados
.skip(n)
Salta N resultados
.project(spec)
Incluye/excluye campos
.unwind(field)
Desdobla arrays en documentos individuales
Accumulators para group
Accumulator
Ejemplo
Resultado
$count
{ total: { $count: true } }
Cantidad de docs en el grupo
$sum
{ revenue: { $sum: 'price' } }
Suma del campo
$avg
{ avgAge: { $avg: 'age' } }
Promedio
$min
{ cheapest: { $min: 'price' } }
Minimo
$max
{ highest: { $max: 'price' } }
Maximo
$push
{ names: { $push: 'name' } }
Array con todos los valores
$first
{ first: { $first: 'name' } }
Primer valor del grupo
$last
{ last: { $last: 'name' } }
Ultimo valor del grupo
Lookup (joins)
orders. aggregate ( )
. lookup ( { from : 'users' , localField : 'userId' , foreignField : '_id' , as : 'user' , single : true } )
. toArray ( ) ;
users. aggregate ( )
. lookup ( { from : 'orders' , localField : '_id' , foreignField : 'userId' , as : 'orders' } )
. toArray ( ) ;
users. aggregate ( )
. lookup ( {
from : 'orders' ,
localField : '_id' ,
foreignField : 'userId' ,
as : 'bigOrders' ,
filter : { price : { $gt : 100 } }
} )
. toArray ( ) ;
orders. aggregate ( )
. lookup ( { from : 'users' , localField : 'userId' , foreignField : '_id' , as : 'user' , single : true } )
. lookup ( { from : 'products' , localField : 'productId' , foreignField : '_id' , as : 'product' , single : true } )
. match ( { 'product.category' : 'hardware' } )
. toArray ( ) ; Encriptacion Full database (at-rest)const adapter = await EncryptedAdapter. create (
new FileStorageAdapter ( './data' ) ,
'my-password'
) ;
const db = new DocStore ( adapter) ;
db. flush ( ) ;
await adapter. persist ( ) ;
const adapter2 = await EncryptedAdapter. create ( innerAdapter, 'my-password' ) ;
await adapter2. preload ( [ 'users.docs.json' , 'users.meta.json' ] ) ;
const db2 = new DocStore ( adapter2) ; Field-level (campos individuales)const fc = await FieldCrypto. create ( 'my-password' ) ;
users. insert ( {
name : 'Alice' ,
city : 'Madrid' ,
ssn : await fc. encrypt ( '123-45-6789' ) ,
creditCard : await fc. encrypt ( '4111-...' ) ,
} ) ;
const doc = users. findOne ( { name : 'Alice' } ) ;
const ssn = await fc. decrypt ( doc. ssn) ;
fc. isEncrypted ( doc. ssn) ;
fc. isEncrypted ( doc. name) ; Autenticacionconst auth = new Auth ( db, { secret : 'jwt-secret-key' } ) ;
await auth. init ( ) ; Registro y loginconst user = await auth. register ( 'alice@test.com' , 'password123' , { name : 'Alice' } ) ;
const { token, user } = await auth. login ( 'alice@test.com' , 'password123' ) ; Verificar tokenconst payload = await auth. verify ( token) ;
RBACauth. assignRole ( userId, 'admin' ) ;
auth. removeRole ( userId, 'admin' ) ;
auth. hasRole ( userId, 'admin' ) ;
const payload = await auth. authorize ( token, 'admin' ) ;
Gestion de usuariosauth. getUser ( userId) ;
auth. getUserByEmail ( 'alice@test.com' ) ;
auth. listUsers ( { roles : { $contains : 'admin' } } , { sort : { createdAt : - 1 } , limit : 10 } ) ;
auth. disableUser ( userId) ;
auth. enableUser ( userId) ;
auth. deleteUser ( userId) ; Passwords y sesionesawait auth. changePassword ( userId, 'old-pass' , 'new-pass' ) ;
await auth. resetPassword ( userId, 'new-pass' ) ;
auth. logout ( token) ;
auth. logoutAll ( userId) ;
auth. cleanExpiredSessions ( ) ; Storage Adapters
new DocStore ( './data' ) ;
new DocStore ( new FileStorageAdapter ( './data' ) ) ;
new DocStore ( new MemoryStorageAdapter ( ) ) ;
const adapter = new CloudflareKVAdapter ( env. MY_KV , 'prefix/' ) ;
await adapter. preload ( [ 'users.docs.json' , 'users.meta.json' ] ) ;
new DocStore ( adapter) ;
const adapter = await EncryptedAdapter. create ( innerAdapter, 'password' ) ;
new DocStore ( adapter) ;
class MyAdapter {
readJson ( filename ) { }
writeJson ( filename, data ) { }
delete ( filename) { }
} Archivos de storageColeccion "users"
├── users.docs.json Documentos: [{ _id, name, age, ... }]
├── users.meta.json Metadata: { indexes: [{ field, type, unique }] }
├── users.email.idx.json Hash index (si existe)
└── users.age.sidx.json Sorted index (si existe) Equivalencias SQL
SQL
js-doc-store
SELECT * FROM users WHERE age > 18
users.find({ age: { $gt: 18 } }).toArray()
SELECT name, age FROM users ORDER BY age DESC LIMIT 10
users.find({}).sort({ age: -1 }).limit(10).project({ name: 1, age: 1 }).toArray()
SELECT COUNT(*) FROM users WHERE city = 'Madrid'
users.count({ city: 'Madrid' })
UPDATE users SET age = 31 WHERE email = 'x'
users.update({ email: 'x' }, { $set: { age: 31 } })
DELETE FROM users WHERE status = 'inactive'
users.removeMany({ status: 'inactive' })
SELECT u.name, SUM(o.price) FROM orders o JOIN users u ON o.userId = u._id GROUP BY u.name
orders.aggregate().lookup({ from: 'users', localField: 'userId', foreignField: '_id', as: 'user', single: true }).group('user.name', { total: { $sum: 'price' } }).toArray()
CREATE UNIQUE INDEX idx ON users(email)
users.createIndex('email', { unique: true })
BenchmarkResultados en Node.js, N=10,000 documentos:
Queries
Operacion
Latencia
Notas
findOne (hash index)
29us
O(1) lookup
hash lookup + limit 10
1.06ms
Indice + clone solo top 10
range index + limit 10
2.42ms
Binary search + limit
full scan + limit 10
1.45ms
Sin indice, early limit
sort indexed + limit 10
4.81ms
SortedIndex, sin sort en memoria
sort in-memory + limit 10
5.15ms
Fallback sin indice
count (con filtro)
1.36ms
Sin allocations
Writes
Operacion
Latencia
insert
47us/doc
update ($inc)
278us/op
flush (10K docs)
424us
Escalabilidad
N docs
insert total
findOne
scan + limit 10
100
7ms
22us
757us
1,000
52ms
16us
6.85ms
5,000
155ms
18us
29.9ms
10,000
470ms
27us
61.2ms
50,000
4.24s
101us
326ms
Optimizaciones internas
structuredClone cuando disponible, JSON fallback
Clone solo en frontera — operaciones internas trabajan con refs raw
Skip+limit antes de clone — solo clona los N resultados finales
SortedIndex en cursor.sort() — evita sort en memoria cuando hay indice
_countMatching — cuenta sin allocar array de resultados
Dirty tracking — _dirtyIds para flush incremental
Comparacion vs D1
Aspecto
js-doc-store
D1
Costo por query
$0 (CPU del Worker)
$0.001/M rows read
Costo por write
$0 (flush a KV)
$1.00/M rows written
Storage
KV: $0.50/GB
$0.75/GB
Max docs
~100K (limite memoria)
Millones
SQL
No (queries MongoDB-style)
Si (SQLite completo)
Joins
lookup() en aggregation
JOIN nativo
ACID
No (eventual consistency)
Si
Portabilidad
Node/browser/Workers/Deno
Solo Cloudflare
Offline
Si
No
Encriptacion
AES-256-GCM built-in
No
Auth
JWT + RBAC built-in
No
Recomendacion : js-doc-store para < 100K docs con portabilidad, encriptacion, o auth integrado. D1 para datasets grandes con SQL complejo.
Tables (schema + validation + views)Capa estilo Airtable sobre DocStore: columnas tipadas, validacion, defaults, autonumber, vistas guardadas, y templates.
Definir un schemaconst { DocStore, MemoryStorageAdapter, Table } = require ( './js-doc-store' ) ;
const db = new DocStore ( new MemoryStorageAdapter ( ) ) ;
const contacts = new Table ( db, 'contacts' , {
columns : [
{ name : 'Name' , type : 'text' , required : true } ,
{ name : 'Email' , type : 'email' , unique : true } ,
{ name : 'Phone' , type : 'phone' } ,
{ name : 'Age' , type : 'number' } ,
{ name : 'Active' , type : 'checkbox' , default : true } ,
{ name : 'Status' , type : 'select' , options : [ 'Lead' , 'Active' , 'Churned' ] } ,
{ name : 'Tags' , type : 'multiselect' , options : [ 'VIP' , 'Enterprise' , 'SMB' ] } ,
{ name : 'Website' , type : 'url' } ,
{ name : 'Company' , type : 'relation' , collection : 'companies' } ,
{ name : 'Number' , type : 'autonumber' } ,
]
} ) ;
contacts. insert ( { Name : 'Alice' , Email : 'alice@test.com' , Status : 'Lead' } ) ;
Tipos de columna
Tipo
Validacion
Ejemplo
text
string
'Hello'
number
number, no NaN
42
checkbox
boolean
true
date
string, number, o Date
'2024-01-15'
email
formato email
'alice@test.com'
url
comienza con http(s)://
'https://example.com'
phone
digitos, espacios, +, -, ()
'+1 555-1234'
select
valor en options[]
'Active'
multiselect
array de valores en options[]
['VIP', 'Enterprise']
relation
_id de doc en otra coleccion
'co-1'
json
cualquier valor
{ key: 'value' }
attachment
string (URL) u objeto
'https://...'
autonumber
auto-incrementa (1, 2, 3...)
no se pasa en insert
formula
campo computado
no se valida
Opciones de columna
Opcion
Efecto
required: true
No puede ser undefined/null/vacio
unique: true
Crea indice unico automaticamente
default: value
Valor por defecto (o funcion: () => Date.now())
options: [...]
Opciones validas para select/multiselect
collection: 'name'
Coleccion relacionada (para type: 'relation')
Views (queries guardadas)
contacts. createView ( 'active-vip' , {
filter : { $and : [ { Status : 'Active' } , { Tags : { $contains : 'VIP' } } ] } ,
sort : { Name : 1 } ,
limit : 50 ,
} ) ;
const results = contacts. view ( 'active-vip' ) ;
contacts. listViews ( ) ;
contacts. getView ( 'active-vip' ) ;
contacts. dropView ( 'active-vip' ) ; Schema managementcontacts. getSchema ( ) ;
contacts. addColumn ( { name : 'Score' , type : 'number' , default : 0 } ) ;
contacts. renameColumn ( 'Score' , 'Rating' ) ;
contacts. removeColumn ( 'Rating' ) ; Relaciones
const doc = contacts. findById ( id) ;
const expanded = contacts. expandRelations ( doc) ;
Templates predefinidosconst { createFromTemplate } = require ( './js-doc-store' ) ;
const crm = createFromTemplate ( db, 'my-crm' , 'crm' ) ;
const tasks = createFromTemplate ( db, 'my-tasks' , 'tasks' ) ;
const inv = createFromTemplate ( db, 'my-inv' , 'inventory' ) ;
const blog = createFromTemplate ( db, 'my-blog' , 'content' ) ;
Template
Columnas incluidas
crm
Name, Email, Phone, Company, Status (Lead/Qualified/Active/Churned), Revenue, Notes, Tags, CreatedAt
tasks
Title, Description, Status (Todo/In Progress/Done/Blocked), Priority (Low-Urgent), Assignee, DueDate, Tags, Number, CreatedAt
inventory
SKU (unique), Name, Category, Price, Stock, Active, ImageURL, Number
content
Title, Body, Author, Status (Draft/Review/Published/Archived), Category, Tags, PublishedAt, URL, Number, CreatedAt
Relacionado
js-vector-store — Vector database para busqueda semantica (embeddings, similarity, RAG). Misma filosofia, mismos adapters.
CreditosCreado por Mauricio Perera
LicenciaMIT