@storion/storion 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/docs/API.md ADDED
@@ -0,0 +1,216 @@
1
+ # API reference
2
+
3
+ ## createDatabase(options)
4
+
5
+ Create or connect to a database. Returns a **Database** instance.
6
+
7
+ - **options.name** (string) – Database name.
8
+ - **options.storage** (`'localStorage' | 'sessionStorage' | 'indexedDB'`) – Storage backend.
9
+ - **options.config** (object, optional) – Config object to create tables from. See [CONFIG_FORMAT.md](./CONFIG_FORMAT.md).
10
+ - **options.storageKey** (string, optional) – Key used in storage (default: `__LS_DB__`).
11
+
12
+ ```js
13
+ const db = await createDatabase({
14
+ name: 'myapp',
15
+ storage: 'localStorage',
16
+ config: { tables: { users: { columns: [{ name: 'id', type: 'int' }, { name: 'email', type: 'string' }] } } }
17
+ });
18
+ ```
19
+
20
+ ---
21
+
22
+ ## loadConfigFromUrl(url)
23
+
24
+ Load a config object from a URL (e.g. `/config/db.json`). Returns a **Promise<object>**.
25
+
26
+ ```js
27
+ const config = await loadConfigFromUrl('/config/db.json');
28
+ const db = await createDatabase({ name: 'myapp', storage: 'localStorage', config });
29
+ ```
30
+
31
+ ---
32
+
33
+ ## loadConfigFromFile(file)
34
+
35
+ Load a config object from a **File** (e.g. from `<input type="file">`). Returns a **Promise&lt;object&gt;**.
36
+
37
+ ```js
38
+ const config = await loadConfigFromFile(fileInput.files[0]);
39
+ const db = await createDatabase({ name: 'imported', storage: 'localStorage', config });
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Database instance
45
+
46
+ ### db.name
47
+
48
+ Read-only. The database name.
49
+
50
+ ### db.createTable(tableName, columns)
51
+
52
+ Create a table. **columns** is an array of `string` (column name, type `string`) or `{ name, type }` with `type` in `'int' | 'float' | 'boolean' | 'string' | 'json'`. An `id` column (type `int`) is added if missing.
53
+
54
+ ```js
55
+ await db.createTable('users', [
56
+ { name: 'id', type: 'int' },
57
+ { name: 'email', type: 'string' },
58
+ { name: 'active', type: 'boolean' }
59
+ ]);
60
+ ```
61
+
62
+ ### db.listTables()
63
+
64
+ Returns **Promise&lt;string[]&gt;** – table names.
65
+
66
+ ### db.getTable(tableName)
67
+
68
+ Returns **Promise&lt;{ columns, rows }&gt;** – table structure and all rows.
69
+
70
+ ### db.insert(tableName, row)
71
+
72
+ Insert a row. `id` is auto-generated if omitted. Returns **Promise&lt;object&gt;** – the inserted row.
73
+
74
+ ```js
75
+ const row = await db.insert('users', { email: 'a@b.com', active: true });
76
+ ```
77
+
78
+ ### db.fetch(tableName, options?)
79
+
80
+ Fetch rows with optional **options**: `filter` (object), `sortBy` (string), `sortOrder` (`'asc' | 'desc'`), `limit` (number). Returns **Promise&lt;object[]&gt;** – rows.
81
+
82
+ ```js
83
+ const rows = await db.fetch('users', { filter: { active: true }, sortBy: 'email', limit: 10 });
84
+ ```
85
+
86
+ ### db.query(tableName, query)
87
+
88
+ Run a JSON query (where, orderBy, limit, offset). Returns **Promise&lt;{ rows, totalCount }&gt;**. See [QUERY_LANGUAGE.md](./QUERY_LANGUAGE.md).
89
+
90
+ ```js
91
+ const { rows, totalCount } = await db.query('users', {
92
+ where: { field: 'status', op: 'eq', value: 'active' },
93
+ orderBy: [{ field: 'name', direction: 'asc' }],
94
+ limit: 20,
95
+ offset: 0
96
+ });
97
+ ```
98
+
99
+ ### db.update(tableName, id, newData)
100
+
101
+ Update a row by `id`. Returns **Promise&lt;object&gt;** – updated row.
102
+
103
+ ```js
104
+ await db.update('users', 1, { email: 'new@b.com' });
105
+ ```
106
+
107
+ ### db.delete(tableName, id)
108
+
109
+ Delete a row by `id`. Returns **Promise&lt;boolean&gt;** – success.
110
+
111
+ ### db.deleteTable(tableName)
112
+
113
+ Delete a table. Fails if another table has a foreign key to it.
114
+
115
+ ### db.exportConfig()
116
+
117
+ Export the current database (and its tables/rows) as a config-like object. Returns **Promise&lt;object&gt;**.
118
+
119
+ ### db.subscribe(callback) / db.subscribe(tableName, callback) / db.subscribe(tableName, rowId, callback)
120
+
121
+ Subscribe to change events. When any code mutates the database (insert, update, delete, createTable, deleteTable), all matching subscribers receive an event. Use this so **multiple components** that share the same `Database` instance can react to changes without polling—e.g. Component A updates a row and Component B (subscribed to that table) receives the event and refreshes its view.
122
+
123
+ - **subscribe(callback)** – subscribe to all changes in this database.
124
+ - **subscribe(tableName, callback)** – subscribe only to changes for `tableName`.
125
+ - **subscribe(tableName, rowId, callback)** – subscribe only to changes for that row.
126
+
127
+ Returns a function **unsubscribe()** – call it to stop receiving events.
128
+
129
+ Every matching subscriber receives the event (multiple components can subscribe to the same table or row). If no one has subscribed, the database behaves as before; subscription is optional.
130
+
131
+ ```js
132
+ const unsubscribe = db.subscribe('todos', (event) => {
133
+ console.log(event.type, event.tableName, event.row);
134
+ // event.type: 'insert' | 'update' | 'delete' | 'tableCreated' | 'tableDeleted'
135
+ // event.row, event.rowId, event.previousRow (for update/delete)
136
+ });
137
+ // later: unsubscribe();
138
+ ```
139
+
140
+ ### db.unsubscribe(id)
141
+
142
+ Remove a subscription by id. Prefer using the function returned from **subscribe()** instead.
143
+
144
+ ### Change event shape (StorionChangeEvent)
145
+
146
+ - **type** – `'insert' | 'update' | 'delete' | 'tableCreated' | 'tableDeleted'`
147
+ - **dbName** – database name
148
+ - **tableName** – table name
149
+ - **row** – inserted/updated row (current state); for delete, see **previousRow**
150
+ - **rowId** – id of the row (for update/delete)
151
+ - **previousRow** – for `update` and `delete`, the row before the change
152
+
153
+ ### db.setChangeBroadcaster(broadcaster)
154
+
155
+ Optional. Set an object with **broadcastChange(event)** to send change events to another context (e.g. for cross-context sync in a Chrome extension). The same event payload is passed to local subscribers and to the broadcaster. Omit or pass `null` to disable.
156
+
157
+ ---
158
+
159
+ ## createChangeListener(transport, onChange)
160
+
161
+ Helper for **receiving** Storion change events from another context (e.g. a Chrome extension, another window, or a background script) over a custom transport.
162
+
163
+ - **transport.onMessage(handler)** – function you provide that registers a message handler and returns an optional unsubscribe function. The handler should be called with messages that are already decoded `StorionChangeEvent`-like objects.
164
+ - **onChange(event)** – callback that will be invoked whenever a valid `StorionChangeEvent` is received.
165
+
166
+ Returns a function **unsubscribe()** that detaches the listener (if the transport provided one).
167
+
168
+ ```js
169
+ // Example: adapter around window.postMessage
170
+ const transport = {
171
+ onMessage(handler) {
172
+ function listener(ev) {
173
+ // assume ev.data is already a StorionChangeEvent from another context
174
+ handler(ev.data);
175
+ }
176
+ window.addEventListener('message', listener);
177
+ return () => window.removeEventListener('message', listener);
178
+ }
179
+ };
180
+
181
+ const stop = createChangeListener(transport, (event) => {
182
+ console.log('Received change from another context:', event);
183
+ // e.g. trigger a UI refresh or sync a local Database instance
184
+ });
185
+
186
+ // later:
187
+ stop();
188
+ ```
189
+
190
+ Only messages that look like a valid `StorionChangeEvent` are forwarded to **onChange**; other messages on the same transport are ignored.
191
+
192
+ ---
193
+
194
+ ## Query engine (standalone)
195
+
196
+ You can use the query engine on raw arrays of rows (e.g. from another source):
197
+
198
+ - **executeQuery(rows, columns, query)** – returns `{ rows, totalCount }`.
199
+ - **validateQuery(query, columns)** – returns `{ valid: boolean, error?: string }`.
200
+ - **QUERY_OPERATORS** – array of allowed operator names.
201
+
202
+ ---
203
+
204
+ ## Schema helpers
205
+
206
+ - **parseConfig(config, dbName?)** – parse a config object into internal `{ databases }` shape.
207
+ - **normalizeColumn(col)** – normalize a column def to `{ name, type }`.
208
+ - **getColumnNames(columns)** – array of column names.
209
+ - **getColumnType(columns, colName)** – type string for a column.
210
+ - **coerceValue(value, type)** – coerce a value to the given type.
211
+
212
+ ---
213
+
214
+ ## getStorageAdapter(type, storageKey?)
215
+
216
+ Returns the low-level adapter for `'localStorage' | 'sessionStorage' | 'indexedDB'`. Used internally; you typically use **createDatabase** only.
@@ -0,0 +1,117 @@
1
+ # Database configuration format
2
+
3
+ You can create a database and its tables from a **config object** when calling `createDatabase()`, or load that config from a URL or file.
4
+
5
+ ## Option 1: Config object in code
6
+
7
+ ```js
8
+ import { createDatabase } from 'storion';
9
+
10
+ const config = {
11
+ tables: {
12
+ users: {
13
+ columns: [
14
+ { name: 'id', type: 'int' },
15
+ { name: 'email', type: 'string' },
16
+ { name: 'name', type: 'string' },
17
+ { name: 'active', type: 'boolean' }
18
+ ]
19
+ },
20
+ posts: {
21
+ columns: [
22
+ { name: 'id', type: 'int' },
23
+ { name: 'title', type: 'string' },
24
+ { name: 'user_id', type: 'int', references: { table: 'users', column: 'id' } }
25
+ ]
26
+ }
27
+ }
28
+ };
29
+
30
+ const db = await createDatabase({
31
+ name: 'myapp',
32
+ storage: 'localStorage',
33
+ config
34
+ });
35
+ ```
36
+
37
+ ## Option 2: Multiple databases in one config
38
+
39
+ ```json
40
+ {
41
+ "databases": {
42
+ "app1": {
43
+ "tables": {
44
+ "users": {
45
+ "columns": [
46
+ { "name": "id", "type": "int" },
47
+ { "name": "email", "type": "string" }
48
+ ]
49
+ }
50
+ }
51
+ },
52
+ "app2": {
53
+ "tables": {
54
+ "items": {
55
+ "columns": [
56
+ { "name": "id", "type": "int" },
57
+ { "name": "label", "type": "string" }
58
+ ]
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ When you call `createDatabase({ name: 'app1', storage: 'localStorage', config })`, only the `app1` database and its tables are created in that instance.
67
+
68
+ ## Option 3: Load config from URL
69
+
70
+ In the browser you can fetch a JSON config from your server or public folder:
71
+
72
+ ```js
73
+ import { createDatabase, loadConfigFromUrl } from 'storion';
74
+
75
+ const config = await loadConfigFromUrl('/config/db.json');
76
+ const db = await createDatabase({
77
+ name: 'myapp',
78
+ storage: 'localStorage',
79
+ config
80
+ });
81
+ ```
82
+
83
+ ## Option 4: Load config from a File (e.g. file input)
84
+
85
+ ```js
86
+ import { createDatabase, loadConfigFromFile } from 'storion';
87
+
88
+ // In your HTML: <input type="file" id="configFile" accept=".json" />
89
+ document.getElementById('configFile').addEventListener('change', async (e) => {
90
+ const file = e.target.files?.[0];
91
+ if (!file) return;
92
+ const config = await loadConfigFromFile(file);
93
+ const db = await createDatabase({
94
+ name: 'imported',
95
+ storage: 'localStorage',
96
+ config
97
+ });
98
+ });
99
+ ```
100
+
101
+ ## Column types
102
+
103
+ - `int` – integer
104
+ - `float` – number
105
+ - `boolean` – true/false
106
+ - `string` – text
107
+ - `json` – JSON value (object or array). Values are stored as parsed JSON; when you pass a string, it will be `JSON.parse`d where possible.
108
+
109
+ ## Optional: foreign key
110
+
111
+ Use `references` to point a column to another table’s column:
112
+
113
+ ```json
114
+ { "name": "user_id", "type": "int", "references": { "table": "users", "column": "id" } }
115
+ ```
116
+
117
+ If you omit `id` in the columns list, an `id` (type `int`) column is added automatically as the primary key.
@@ -0,0 +1,73 @@
1
+ # Query language
2
+
3
+ The package uses a **JSON query object** to filter and sort table data. Use it with `db.query(tableName, query)`.
4
+
5
+ ## Query structure
6
+
7
+ ```json
8
+ {
9
+ "where": { ... },
10
+ "orderBy": [ ... ],
11
+ "limit": 1000,
12
+ "offset": 0
13
+ }
14
+ ```
15
+
16
+ | Key | Type | Description |
17
+ |-----------|--------|-------------|
18
+ | `where` | object | Optional. Filter conditions. Omit or `null` = no filter. |
19
+ | `orderBy` | array | Optional. Sort by one or more columns. |
20
+ | `limit` | number | Optional. Max rows (non-negative integer). |
21
+ | `offset` | number | Optional. Skip N rows (non-negative integer). |
22
+
23
+ ## Where clause
24
+
25
+ `where` can be:
26
+
27
+ 1. **A single condition** – `{ "field": "columnName", "op": "eq", "value": 42 }`
28
+ 2. **Logic node** – `{ "and": [ ... ] }` or `{ "or": [ ... ] }` (arrays of conditions or nested logic).
29
+
30
+ ### Operators
31
+
32
+ | Operator | Description | `value` required |
33
+ |----------------|--------------------------------|-------------------|
34
+ | `eq` | Equals | Yes (except null) |
35
+ | `ne` | Not equals | Yes |
36
+ | `gt` | Greater than | Yes |
37
+ | `gte` | Greater than or equal | Yes |
38
+ | `lt` | Less than | Yes |
39
+ | `lte` | Less than or equal | Yes |
40
+ | `contains` | String contains (case-insensitive) | Yes |
41
+ | `startsWith` | String starts with (case-insensitive) | Yes |
42
+ | `endsWith` | String ends with (case-insensitive) | Yes |
43
+ | `in` | Value in list | Yes (array) |
44
+ | `notIn` | Value not in list | Yes (array) |
45
+ | `isNull` | Value is null/undefined | No |
46
+ | `isNotNull` | Value is not null/undefined | No |
47
+
48
+ String comparisons are **case-insensitive**. Column types (`int`, `float`, `boolean`, `string`, `json`) are used for type-aware comparison. For `json` columns, comparisons use the JSON string representation (e.g. `JSON.stringify`), so equality and string operators work on the serialized form of the value.
49
+
50
+ ## OrderBy
51
+
52
+ Array of `{ "field": "columnName", "direction": "asc" | "desc" }`. Multiple columns supported; earlier entries have higher priority.
53
+
54
+ ## Example with db.query()
55
+
56
+ ```js
57
+ const { rows, totalCount } = await db.query('users', {
58
+ where: {
59
+ and: [
60
+ { field: 'status', op: 'eq', value: 'active' },
61
+ { field: 'name', op: 'contains', value: 'smith' }
62
+ ]
63
+ },
64
+ orderBy: [
65
+ { field: 'created_at', direction: 'desc' },
66
+ { field: 'id', direction: 'asc' }
67
+ ],
68
+ limit: 20,
69
+ offset: 0
70
+ });
71
+ ```
72
+
73
+ Returns `{ rows: [...], totalCount: number }`.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@storion/storion",
3
+ "version": "1.0.0",
4
+ "description": "Framework-agnostic client-side database with localStorage, sessionStorage, and IndexedDB. Create databases, tables, save/fetch records, and run JSON queries.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "types",
18
+ "README.md",
19
+ "docs"
20
+ ],
21
+ "scripts": {
22
+ "build": "node build.js",
23
+ "build:cdn": "node build-cdn.js",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "browser",
28
+ "database",
29
+ "localStorage",
30
+ "sessionStorage",
31
+ "indexeddb",
32
+ "query",
33
+ "offline",
34
+ "storage",
35
+ "framework-agnostic",
36
+ "react",
37
+ "vue",
38
+ "angular"
39
+ ],
40
+ "author": "",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/storionjs/storion.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/storionjs/storion/issues"
48
+ },
49
+ "homepage": "https://github.com/storionjs/storion#readme",
50
+ "sideEffects": false,
51
+ "devDependencies": {
52
+ "esbuild": "^0.24.0"
53
+ }
54
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Type declarations for storion.
3
+ * Database instance methods are async and return Promises.
4
+ */
5
+
6
+ export type StorageType = 'localStorage' | 'sessionStorage' | 'indexedDB';
7
+
8
+ export interface CreateDatabaseOptions {
9
+ name: string;
10
+ storage: StorageType;
11
+ /** Optional config object to create DB and tables from */
12
+ config?: DBConfig;
13
+ /** Optional storage key (default: __LS_DB__) */
14
+ storageKey?: string;
15
+ }
16
+
17
+ export interface DBConfig {
18
+ databases?: Record<string, { tables?: Record<string, TableDef> }>;
19
+ tables?: Record<string, TableDef>;
20
+ }
21
+
22
+ export interface TableDef {
23
+ columns: Array<string | { name: string; type: 'int' | 'float' | 'boolean' | 'string' | 'json'; references?: { table: string; column: string } }>;
24
+ }
25
+
26
+ export interface QueryWhere {
27
+ field?: string;
28
+ op?: string;
29
+ value?: unknown;
30
+ and?: QueryWhere[];
31
+ or?: QueryWhere[];
32
+ }
33
+
34
+ export interface Query {
35
+ where?: QueryWhere | null;
36
+ orderBy?: Array<{ field: string; direction: 'asc' | 'desc' }>;
37
+ limit?: number;
38
+ offset?: number;
39
+ }
40
+
41
+ export interface QueryResult {
42
+ rows: Record<string, unknown>[];
43
+ totalCount: number;
44
+ }
45
+
46
+ export interface FetchOptions {
47
+ filter?: Record<string, unknown>;
48
+ sortBy?: string;
49
+ sortOrder?: 'asc' | 'desc';
50
+ limit?: number;
51
+ }
52
+
53
+ /** Change event emitted after insert, update, delete, createTable, or deleteTable. */
54
+ export interface StorionChangeEvent {
55
+ type: 'insert' | 'update' | 'delete' | 'tableCreated' | 'tableDeleted';
56
+ dbName: string;
57
+ tableName: string;
58
+ row?: Record<string, unknown>;
59
+ rowId?: number | string;
60
+ previousRow?: Record<string, unknown>;
61
+ }
62
+
63
+ /** Generic transport interface for receiving change events from another context. */
64
+ export interface ChangeTransport {
65
+ /**
66
+ * Register a message handler. The handler will be called with messages that
67
+ * should represent StorionChangeEvent-like objects. Returns an optional
68
+ * function that can be called to unsubscribe.
69
+ */
70
+ onMessage(handler: (message: unknown) => void): (() => void) | void;
71
+ }
72
+
73
+ /** Optional broadcaster for cross-context sync (e.g. extension ↔ webapp). */
74
+ export interface ChangeBroadcaster {
75
+ broadcastChange(event: StorionChangeEvent): void | Promise<void>;
76
+ }
77
+
78
+ export function createDatabase(options: CreateDatabaseOptions): Promise<Database>;
79
+
80
+ export function loadConfigFromUrl(url: string): Promise<DBConfig>;
81
+
82
+ export function loadConfigFromFile(file: File): Promise<DBConfig>;
83
+
84
+ export function getStorageAdapter(type: StorageType, storageKey?: string): StorageAdapter;
85
+
86
+ export function executeQuery(rows: object[], columns: unknown[], query: Query | null): QueryResult;
87
+
88
+ export function validateQuery(query: Query | null, columns: unknown[]): { valid: boolean; error?: string };
89
+
90
+ export const QUERY_OPERATORS: string[];
91
+
92
+ export function parseConfig(config: DBConfig, dbName?: string): { databases: Record<string, unknown> };
93
+
94
+ export function normalizeColumn(col: string | { name: string; type?: string }): { name: string; type: string } | null;
95
+
96
+ export function getColumnNames(columns: unknown[]): string[];
97
+
98
+ export function getColumnType(columns: unknown[], colName: string): string;
99
+
100
+ export function coerceValue(value: unknown, type: string): unknown;
101
+
102
+ export interface StorageAdapter {
103
+ getItem(): string | null | Promise<string | null>;
104
+ setItem(key: null, value: string): void | boolean | Promise<void | boolean>;
105
+ removeItem(): void | boolean | Promise<void | boolean>;
106
+ getAllKeys(): string[] | Promise<string[]>;
107
+ isAsync?: boolean;
108
+ }
109
+
110
+ export interface Database {
111
+ readonly name: string;
112
+ createTable(tableName: string, columns: Array<string | { name: string; type: string }>): Promise<boolean>;
113
+ listTables(): Promise<string[]>;
114
+ getTable(tableName: string): Promise<{ columns: unknown[]; rows: Record<string, unknown>[] }>;
115
+ insert(tableName: string, row: Record<string, unknown>): Promise<Record<string, unknown>>;
116
+ fetch(tableName: string, options?: FetchOptions): Promise<Record<string, unknown>[]>;
117
+ query(tableName: string, query: Query): Promise<QueryResult>;
118
+ update(tableName: string, id: number | string, newData: Record<string, unknown>): Promise<Record<string, unknown>>;
119
+ delete(tableName: string, id: number | string): Promise<boolean>;
120
+ deleteTable(tableName: string): Promise<boolean>;
121
+ exportConfig(): Promise<DBConfig>;
122
+ /** Subscribe to all changes in this database. Returns unsubscribe function. */
123
+ subscribe(callback: (event: StorionChangeEvent) => void): () => void;
124
+ /** Subscribe to changes for one table. Returns unsubscribe function. */
125
+ subscribe(tableName: string, callback: (event: StorionChangeEvent) => void): () => void;
126
+ /** Subscribe to changes for one row. Returns unsubscribe function. */
127
+ subscribe(tableName: string, rowId: number | string, callback: (event: StorionChangeEvent) => void): () => void;
128
+ /** Remove subscription by id (prefer using the function returned from subscribe). */
129
+ unsubscribe(id: number): void;
130
+ /** Set optional broadcaster for cross-context sync (Phase 2). */
131
+ setChangeBroadcaster(broadcaster: ChangeBroadcaster | null): void;
132
+ }
133
+
134
+ /**
135
+ * Create a listener for change events coming from another context (e.g. from
136
+ * a Chrome extension or another window) via a user-provided transport.
137
+ * Returns a function to unsubscribe.
138
+ */
139
+ export function createChangeListener(
140
+ transport: ChangeTransport,
141
+ onChange: (event: StorionChangeEvent) => void
142
+ ): () => void;