Coder Social home page Coder Social logo

tinyplex / tinybase Goto Github PK

View Code? Open in Web Editor NEW
3.0K 26.0 59.0 200.43 MB

The reactive data store for local‑first apps.

Home Page: https://tinybase.org

License: MIT License

JavaScript 39.78% TypeScript 59.59% Less 0.63%
react javascript typescript data reactive

tinybase's Introduction

The reactive data store for local-first apps.

Build blisteringly fast web apps that work both online and offline. Manage your state locally, synchronize it to the cloud when you need to, or even make it collaborative. But, most importantly... have fun building stuff again!

NEW! v4.8 release "The One With PowerSync"

Get started

Try the demos

Read the docs



TinyBase works great on its own, but also plays well with friends!




Start with a simple key-value store.

Creating a Store requires just a simple call to the createStore function. Once you have one, you can easily set Values in it by unique Id. And of course you can easily get them back out again.

Read more about using keyed value data in The Basics guide.

const store = createStore()
  .setValues({employees: 3})
  .setValue('open', true);

console.log(store.getValues());
// -> {employees: 3, open: true}

Level up to use tabular data.

For other types of data applications, a tabular data structure is more useful. TinyBase lets you set and get nested Table, Row, or Cell data, by unique Id - and in the same Store as the keyed values!

Read more about setting and changing data in The Basics guide.

store
  .setTable('pets', {fido: {species: 'dog'}})
  .setCell('pets', 'fido', 'color', 'brown');

console.log(store.getRow('pets', 'fido'));
// -> {species: 'dog', color: 'brown'}

Register granular listeners.

The magic starts to happen when you register listeners on a Value, Table, Row, or Cell. They get called when any part of that object changes. You can also use wildcards - useful when you don't know the Id of the objects that might change.

Read more about listeners in the Listening To Stores guide.

const listenerId = store.addTableListener('pets', () =>
  console.log('changed'),
);

store.setCell('pets', 'fido', 'sold', false);
// -> 'changed'

store.delListener(listenerId);

Call hooks to bind to data.

If you're using React in your application, the optional ui-react module provides hooks to bind to the data in a Store.

More magic! The useCell hook in this example fetches the dog's color. But it also registers a listener on that cell that will fire and re-render the component whenever the value changes.

Basically you simply describe what data you want in your user interface and TinyBase will take care of the whole lifecycle of updating it for you.

Read more about the using hooks in the Using React Hooks guide.

const App1 = () => {
  const color = useCell('pets', 'fido', 'color', store);
  return <>Color: {color}</>;
};

const app = document.createElement('div');
const root = ReactDOMClient.createRoot(app);
root.render(<App1 />);
console.log(app.innerHTML);
// -> 'Color: brown'

store.setCell('pets', 'fido', 'color', 'walnut');
console.log(app.innerHTML);
// -> 'Color: walnut'

Pre-built reactive components.

The ui-react module provides bare React components that let you build up a fully reactive user interface based on a Store.

For web applications in particular, the new ui-react-dom module provides pre-built components for tabular display of your data, with lots of customization and interactivity options.

Try them out in the UI Components demos, and read more about the underlying ui-react module in the Building UIs guides.

An inspector for your data.

If you are building a web application, the new StoreInspector component lets you overlay a view of the data in your Store, Indexes, Relationships, and so on. You can even edit the data in place and see it update in your app immediately.

Read more about this powerful new tool in the Inspecting Data guide.

Apply schemas to tables & values.

By default, a Store can contain any arbitrary Value, and a Row can contain any arbitrary Cell. But you can add a ValuesSchema or a TablesSchema to a Store to ensure that the values are always what you expect: constraining their types, and providing defaults.

In this example, we set a new Row without the sold Cell in it. The schema ensures it's present with default of false.

Read more about schemas in the Using Schemas guide.

store.setTablesSchema({
  pets: {
    species: {type: 'string'},
    color: {type: 'string'},
    sold: {type: 'boolean', default: false},
  },
});

store.setRow('pets', 'polly', {species: 'parrot'});
console.log(store.getRow('pets', 'polly'));
// -> {species: 'parrot', sold: false}

store.delTablesSchema();

Persist to storage, SQLite, CRDTs.

You can easily persist a Store between browser page reloads or sessions. You can also synchronize it with a web endpoint, or (if you're using TinyBase in an appropriate environment), load and save it to a file. New in v4.0, you can bind TinyBase to SQLite via a range of modules, or to Yjs or Automerge CRDT documents.

Read more about persisters in the Persisting Data guide.

const persister = createSessionPersister(store, 'demo');
await persister.save();

console.log(sessionStorage.getItem('demo'));
// ->
`
[
  {
    "pets":{
      "fido":{"species":"dog","color":"walnut","sold":false},
      "polly":{"species":"parrot","sold":false}
    }
  },
  {"employees":3,"open":true}
]
`;

persister.destroy();
sessionStorage.clear();

Build complex queries with TinyQL.

The Queries object lets you query data across tables, with filtering and aggregation - using a SQL-adjacent syntax called TinyQL.

Accessors and listeners let you sort and paginate the results efficiently, making building rich tabular interfaces easier than ever.

In this example, we have two tables: of pets and their owners. They are joined together by the pet's ownerId Cell. We select the pet's species, and the owner's state, and then aggregate the prices for the combinations.

We access the results by descending price, essentially answering the question: "which is the highest-priced species, and in which state?"

Needless to say, the results are reactive too! You can add listeners to queries just as easily as you do to raw tables.

Read more about Queries in the v2.0 Release Notes, the Making Queries guide, and the Car Analysis demo and Movie Database demo.

store
  .setTable('pets', {
    fido: {species: 'dog', ownerId: '1', price: 5},
    rex: {species: 'dog', ownerId: '2', price: 4},
    felix: {species: 'cat', ownerId: '2', price: 3},
    cujo: {species: 'dog', ownerId: '3', price: 4},
  })
  .setTable('owners', {
    1: {name: 'Alice', state: 'CA'},
    2: {name: 'Bob', state: 'CA'},
    3: {name: 'Carol', state: 'WA'},
  });

const queries = createQueries(store);
queries.setQueryDefinition(
  'prices',
  'pets',
  ({select, join, group}) => {
    select('species');
    select('owners', 'state');
    select('price');
    join('owners', 'ownerId');
    group('price', 'avg').as('avgPrice');
  },
);

queries
  .getResultSortedRowIds('prices', 'avgPrice', true)
  .forEach((rowId) => {
    console.log(queries.getResultRow('prices', rowId));
  });
// -> {species: 'dog', state: 'CA', avgPrice: 4.5}
// -> {species: 'dog', state: 'WA', avgPrice: 4}
// -> {species: 'cat', state: 'CA', avgPrice: 3}

queries.destroy();

Define metrics and aggregations.

A Metrics object makes it easy to keep a running aggregation of Cell values in each Row of a Table. This is useful for counting rows, but also supports averages, ranges of values, or arbitrary aggregations.

In this example, we create a new table of the pet species, and keep a track of which is most expensive. When we add horses to our pet store, the listener detects that the highest price has changed.

Read more about Metrics in the Using Metrics guide.

store.setTable('species', {
  dog: {price: 5},
  cat: {price: 4},
  worm: {price: 1},
});

const metrics = createMetrics(store);
metrics.setMetricDefinition(
  'highestPrice', // metricId
  'species', //      tableId to aggregate
  'max', //          aggregation
  'price', //        cellId to aggregate
);

console.log(metrics.getMetric('highestPrice'));
// -> 5

metrics.addMetricListener('highestPrice', () =>
  console.log(metrics.getMetric('highestPrice')),
);
store.setCell('species', 'horse', 'price', 20);
// -> 20

metrics.destroy();

Create indexes for fast lookups.

An Indexes object makes it easy to look up all the Row objects that have a certain value in a Cell.

In this example, we create an index on the species Cell values. We can then get the the list of distinct Cell value present for that index (known as 'slices'), and the set of Row objects that match each value.

Indexes objects are reactive too. So you can set listeners on them just as you do for the data in the underlying Store.

Read more about Indexes in the Using Indexes guide.

const indexes = createIndexes(store);
indexes.setIndexDefinition(
  'bySpecies', // indexId
  'pets', //      tableId to index
  'species', //   cellId to index
);

console.log(indexes.getSliceIds('bySpecies'));
// -> ['dog', 'cat']
console.log(indexes.getSliceRowIds('bySpecies', 'dog'));
// -> ['fido', 'rex', 'cujo']

indexes.addSliceIdsListener('bySpecies', () =>
  console.log(indexes.getSliceIds('bySpecies')),
);
store.setRow('pets', 'lowly', {species: 'worm'});
// -> ['dog', 'cat', 'worm']

indexes.destroy();

Model table relationships.

A Relationships object lets you associate a Row in a local Table with the Id of a Row in a remote Table. You can also reference a table to itself to create linked lists.

In this example, the species Cell of the pets Table is used to create a relationship to the species Table, so that we can access the price of a given pet.

Like everything else, you can set listeners on Relationships too.

Read more about Relationships in the Using Relationships guide.

const relationships = createRelationships(store);
relationships.setRelationshipDefinition(
  'petSpecies', // relationshipId
  'pets', //       local tableId to link from
  'species', //    remote tableId to link to
  'species', //    cellId containing remote key
);

console.log(
  store.getCell(
    relationships.getRemoteTableId('petSpecies'),
    relationships.getRemoteRowId('petSpecies', 'fido'),
    'price',
  ),
);
// -> 5

relationships.destroy();

Set checkpoints for an undo stack.

A Checkpoints object lets you set checkpoints on a Store. Move forward and backward through them to create undo and redo functions.

In this example, we set a checkpoint, then sell one of the pets. Later, the pet is brought back to the shop, and we go back to that checkpoint to revert the store to its previous state.

Read more about Checkpoints in the Using Checkpoints guide.

const checkpoints = createCheckpoints(store);

store.setCell('pets', 'felix', 'sold', false);
checkpoints.addCheckpoint('pre-sale');

store.setCell('pets', 'felix', 'sold', true);
console.log(store.getCell('pets', 'felix', 'sold'));
// -> true

checkpoints.goBackward();
console.log(store.getCell('pets', 'felix', 'sold'));
// -> false

Type definitions & ORM-like APIs

TinyBase has comprehensive type definitions, and even offers definitions that infer API types from the data schemas you apply.

Furthermore, you can easily create TypeScript .d.ts definitions that model your data and encourage type-safety when reading and writing data - as well as .ts implementations that provide ORM-like methods for your named tables.

Read more about type support in the TinyBase and TypeScript guide.

const tools = createTools(store);
const [dTs, ts] = tools.getStoreApi('shop');

// -- shop.d.ts --
/* Represents the 'pets' Table. */
export type PetsTable = {[rowId: Id]: PetsRow};
/* Represents a Row when getting the content of the 'pets' Table. */
export type PetsRow = {species: string /* ... */};
//...

// -- shop.ts --
export const createShop: typeof createShopDecl = () => {
  //...
};

Did we say tiny?

If you use the basic store module alone, you'll only add a gzipped 5.0kB to your app. Incrementally add the other modules as you need more functionality, or get it all for 9.4kB.

The optional ui-react module is just another 4.3kB, the auxiliary tools module is 11.1kB, and everything is super fast. Life's easy when you have zero dependencies!

Read more about how TinyBase is structured and packaged in the Architecture guide.

 .js.gz.jsdebug.js.d.ts
store5.0kB12.0kB51.6kB252.6kB
metrics1.8kB3.6kB15.2kB30.9kB
indexes1.9kB3.8kB17.0kB35.6kB
relationships1.9kB3.7kB17.1kB44.2kB
queries2.8kB5.7kB25.6kB126.0kB
checkpoints1.6kB3.1kB12.7kB35.1kB
persisters0.8kB1.5kB5.3kB44.1kB
common0.1kB0.1kB0.1kB3.5kB
tinybase (all)9.4kB23.2kB100.8kB0.3kB

Well tested and documented.

TinyBase has 100.0% test coverage, including the code throughout the documentation - even on this page! The guides, demos, and API examples are designed to make it as easy as possible for you to get your TinyBase-powered app up and running.

Read more about how TinyBase is tested in the Unit Testing guide.

 TotalTestedCoverage
Lines1,9681,968100.0%
Statements2,1272,127100.0%
Functions848848100.0%
Branches719719100.0%
Tests4,078
Assertions18,366

Proud to be sponsored by:

Excited to be used by:


Get started

Try the demos

Read the docs


About

Modern apps deserve better. Why trade reactive user experiences to be able to use relational data? Or sacrifice features for bundle size? And why does the cloud do all the work anyway?

Building TinyBase was originally an interesting exercise for me in API design, minification, and documentation. But now it has taken on a life of its own, and has grown beyond my wildest expectations.

It could not have been built without these great projects and friends, and I hope you enjoy using it as much as I do building it!

The story

tinybase's People

Contributors

alanjhughes avatar alitnk avatar alokmenghrajani avatar aretrace avatar arnaudhillen avatar bigab avatar bndkt avatar devjume avatar doubleinc avatar humphd avatar jamesgpearce avatar mutewinter avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tinybase's Issues

Append new cell to an existing row

Is your feature request related to a problem? Please describe.

The .setCell() should update an existing row if it already exist and not replace it. Or, maybe a .appendRow() method?

If i've defined a table like below for example:

import { createStore } from "tinybase";
const store = createStore();

store.setTable("books", {
  1: {
    title: "Book Title",
    author: "John Doe",
  },
});

And, i do:

store.setCell("books", 1, "price", "$15");

This should replace the entire row with the new cell, rather than update it.

Describe the solution you'd like
If i use the .setCell() function for an existing row, it should update the row.

Describe alternatives you've considered
Or maybe a .appendRow() function to append new cells to a row

Importing tinybase/ui-react with React Native / Metro

Describe the bug

Metro doesn't work with the package.json exports field, so we already use the react-native field in TinyBase to define the entry point for React Native. This is fine until you need to import tinybase/ui-react, at which point Metro will fail to resolve it tinybase/ui-react but TypeScript won't complain. If you import tinybase/lib/ui-react to appease Metro, then TypeScript can't find the types.

One possible solution here is to use the typesVersions field in package.json to point to the types for tinybase/lib/ui-react. This works for now, without breaking anything, but it does lead to a divergence in the API and a quirk to document for React Native users.

Your Example Website or App

https://github.com/brentvatne/example-tinybase

Steps to Reproduce the Bug or Issue

  1. Clone the repository (it's a bare-bones React Native / Expo project with TypeScript installed)
  2. Open up App.tsx, notice the type error on the import
    image
  3. See in the commented out import above that we can resolve this type error by importing from tinybase/ui-react instead, but then when you run the app Metro will error.

Expected behavior

As a user, I expect to be able to import tinybase/ui-react in a React Native environment and everything will work - but Metro fails to resolve it. Short of that being possible, I expect there to be another suggested import format that will be compatible with React Native and allow me to use ui-react with Metro and TypeScript.

Screenshots or Videos

No response

Platform

Additional context

Issue previously described in this comment: #28 (comment)

TinyBase does not work in Microsoft Edge

Describe the bug

image

Your Example Website or App

Steps to Reproduce the Bug or Issue

Expected behavior

Screenshots or Videos

No response

Platform

Additional context

No response

Chained/nested joins

I want to create a query over multiple tables, let's say orders, customers, and countries, to retrieve a list of orders along with customer information and the country information of each customer.
The orders table has a foreign key customer_id referencing the customers table, and the customers table has a foreign key country_id referencing the countries table.

In SQL, you can achieve this using nested LEFT JOINs like this:

SELECT
    o.order_id,
    c.customer_name,
    co.country_name
FROM
    orders o
LEFT JOIN
    customers c ON o.customer_id = c.customer_id
LEFT JOIN
    countries co ON c.country_id = co.country_id;

In Tinybase, it seems like I cannot replicate the second join condition.
If I do join("countries", "country_id"), I don't get any results.

Perhaps the join condition could allow for something like join("countries", "customers.country_id")?

Any thoughts?

Consider a select('*') or equivalent

Without Select, queries don'f fetch anything. And since Select('*') is not supported there isn't a way to use getResultTable to return the row.

Think about whether this is for the root table or for every table referenced and how to deal with duplicate cell Ids

Via @founderYonz

node.js getting started example does not work

Hi everyone,

if I follow the get started guide for node.js the following error gets thrown:

file:///Users/user/dev/projectname/node_modules/tinybase/lib/tinybase.js:1
import{promises as e,watch as t}from"fs";const s=e=>typeof e,n=s(""),o=s(!0),r=s(0),a=s(s),i=(e,t)=>e.includes(t),d=(e,t)=>e.every(((s,n)=>0==n||t(e[n-1],s)<=0)),l=(e,t)=>e.sort(t),c=(e,t)=>e.forEach(t),u=e=>f(e,((e,t)=>e+t),0),h=e=>e.length,g=e=>0==h(e),f=(e,t,s)=>e.reduce(t,s),L=e=>e.slice(1),p=e=>JSON.stringify(e,((e,t)=>y(t,Map)?f([...t],((e,[t,s])=>(e[t]=s,e)),{}):t)),w=JSON.parse,v=Math.max,I=Math.min,S=isFinite,y=(e,t)=>e instanceof t,R=e=>null==e,T=(e,t,s)=>R(e)?s?.():t(e),b=e=>e==n||e==o,C=e=>s(e)==a,m=()=>{},E=e=>e.size,k=(e,t)=>e?.has(t)??!1,M=e=>R(e)||0==E(e),A=e=>[...e?.values()??[]],x=e=>e.clear(),D=(e,t)=>e?.forEach(t),J=(e,t)=>e?.delete(t),F=e=>new Map(e),z=(e=F)=>[e(),e()],N=e=>[...e?.keys()??[]],O=(e,t)=>e?.get(t),j=(e,t)=>D(e,((e,s)=>t(s,e))),P=(e,t,s)=>R(s)?(J(e,t),e):e?.set(t,s),B=(e,t,s,n)=>(k(e,t)||(n?.(s),e.set(t,s)),O(e,t)),H=(e,t)=>{const s={},n=t??(e=>e);return D(e,((e,t)=>s[t]=n(e))),s},W=(e,t)=>{const s=F(),n=t??(e=>e);return D(e,((e,t)=>s.set(t,n(e)))),s},q=e=>new Set(e),G=(e,t)=>e?.add(t),K=(e,t,s)=>{const n=e.hasRow,o=F(),r=F(),a=F(),i=F(),d=F(),l=t=>T(O(d,t),(s=>{D(s,e.delListener),P(d,t)})),u=e=>{P(o,e),P(r,e),P(a,e),P(i,e),l(e)};return[()=>e,()=>N(o),e=>j(r,e),e=>k(r,e),e=>O(o,e),e=>O(r,e),(e,t)=>P(r,e,t),(u,h,g,f,L)=>{const p=F(),w=F();P(o,u,h),k(r,u)||(P(r,u,t()),P(a,u,F()),P(i,u,F()));const v=O(a,u),I=O(i,u),S=t=>{const o=s=>e.getCell(h,t,s),r=O(v,t),a=n(h,t)?s(f(o,t)):void 0;if(r!=a&&P(p,t,[r,a]),!R(L)){const e=O(I,t),s=n(h,t)?L(o,t):void 0;e!=s&&P(w,t,s)}},y=e=>{g((()=>{D(p,(([,e],t)=>P(v,t,e))),D(w,((e,t)=>P(I,t,e)))}),p,w,v,I,e),x(p),x(w)};j(v,S),e.hasTable(h)&&c(e.getRowIds(h),(e=>{k(v,e)||S(e)})),y(!0),l(u),P(d,u,q([e.addRowListener(h,null,((e,t,s)=>S(s))),e.addTableListener(h,(()=>y()))]))},u,()=>j(d,u)]},Q=(e,t)=>s(e)==n?t=>t(e):e??(()=>t??""),U=e=>{const t=new WeakMap;return s=>(t.has(s)||t.set(s,e(s)),t.get(s))},V=(e,t,s)=>h(s)<2?G(g(s)?e:B(e,s[0],q()),t):V(B(e,s[0],F()),t,L(s)),X=e=>{const t=(s,n,...o)=>T(s,(s=>g(o)?e(s,n):c([o[0],null],(e=>t(O(s,e),n,...L(o))))));return t},Y=e=>{let t,s=0;const n=[],o=F();return[(r,a,i=[])=>{t??=e();const d=n.pop()??""+s++;return P(o,d,[r,a,i]),V(a,d,i),d},(e,s=[],...n)=>X(D)(e,(e=>T(O(o,e),(([e])=>e(t,...s,...n)))),...s),e=>T(O(o,e),(([,t,s])=>(X(J)(t,e,...s),P(o,e),h(n)<1e3&&n.push(e),s)),(()=>[])),(e,s,n)=>T(O(o,e),(([e,,o])=>{const r=(...a)=>{const i=h(a);i==h(o)?e(t,...a,...n(a)):R(o[i])?c(s[i](...a),(e=>r(...a,e))):r(...a,o[i])};r()}))]},Z=Object,$=Z.keys,_=Z.isFrozen,ee=Z.freeze,te=(e,t)=>!R(((e,t)=>T(e,(e=>e[t])))(e,t)),se=(e,t)=>delete e[t],ne=(e,t)=>c(Z.entries(e),(([e,s])=>t(s,e))),oe=U((e=>{let t,s,n,o=100,r=F(),a=1;const d=q(),l=F(),[u,f,L]=Y((()=>W)),p=F(),w=F(),v=[],I=[],S=(t,s)=>{a=0,e.transaction((()=>D(O(p,s),((s,n)=>D(s,((s,o)=>D(s,((s,r)=>R(s[t])?e.delCell(n,o,r,!0):e.setCell(n,o,r,s[t]))))))))),a=1},y=e=>{P(p,e),P(w,e),f(l,[e])},b=(e,t)=>c(((e,t)=>e.splice(0,t))(e,t??h(e)),y),C=()=>b(v,h(v)-o),m=e.addCellListener(null,null,null,((e,s,o,i,d,l)=>{if(a){T(t,(()=>{v.push(t),C(),b(I),t=void 0,n=1}));const e=B(r,s,F()),a=B(e,o,F()),c=B(a,i,[void 0,void 0],(e=>e[0]=l));c[1]=d,c[0]===c[1]&&M(P(a,i))&&M(P(e,o))&&M(P(r,s))&&(t=v.pop(),n=1),J()}})),E=(e="")=>(R(t)&&(t=""+s++,P(p,t,r),N(t,e),r=F(),n=1),t),A=()=>{g(v)||(I.unshift(E()),S(0,t),t=v.pop(),n=1)},x=()=>{g(I)||(v.push(t),t=I.shift(),S(1,t),n=1)},J=()=>{n&&(f(d),n=0)},z=e=>{const t=E(e);return J(),t},N=(e,t)=>(H(e)&&O(w,e)!==t&&(P(w,e,t),f(l,[e])),W),H=e=>k(p,e),W={setSize:e=>(o=e,C(),W),addCheckpoint:z,setCheckpoint:N,getStore:()=>e,getCheckpointIds:()=>[[...v],t,[...I]],forEachCheckpoint:e=>j(w,e),hasCheckpoint:H,getCheckpoint:e=>O(w,e),goBackward:()=>(A(),J(),W),goForward:()=>(x(),J(),W),goTo:e=>{const s=i(v,e)?A:i(I,e)?x:null;for(;!R(s)&&e!=t;)s();return J(),W},addCheckpointIdsListener:e=>u(e,d),addCheckpointListener:(e,t)=>u(t,l,[e]),delListener:e=>(L(e),W),clear:()=>(b(v),b(I),R(t)||y(t),t=void 0,s=0,z(),W),destroy:()=>{e.delListener(m)},getListenerStats:()=>({})};return ee(W.clear())})),re=(e,t)=>e<t?-1:1,ae=U((e=>{const t=F(),s=F(),[n,o,r,a,i,c,u,h,g,f]=K(e,F,(e=>R(e)?"":e+"")),[L,p,w]=Y((()=>I)),v=(t,s,n)=>{const o=i(t);D(n,((t,n)=>s(n,(s=>D(t,(t=>s(t,(s=>e.forEachCell(o,t,s)))))))))},I={setIndexDefinition:(e,n,o,r,a,i=re)=>{const g=R(a)?void 0:([e],[t])=>a(e,t);return h(e,n,((n,o,a,h,f,L)=>{let w=0;const v=q(),I=q(),S=c(e);if(D(o,(([e,t],s)=>{R(e)||(G(v,e),T(O(S,e),(t=>{J(t,s),M(t)&&(P(S,e),w=1)}))),R(t)||(G(v,t),k(S,t)||(P(S,t,q()),w=1),G(O(S,t),s),R(r)||G(I,t))})),n(),M(f)||(L?j(S,(e=>G(I,e))):j(a,(e=>T(O(h,e),(e=>G(I,e))))),D(I,(e=>{const t=(t,s)=>i(O(f,t),O(f,s),e),s=[...O(S,e)];d(s,t)||(P(S,e,q(l(s,t))),G(v,e))}))),(w||L)&&!R(g)){const t=[...S];d(t,g)||(u(e,F(l(t,g))),w=1)}w&&p(t,[e]),D(v,(t=>p(s,[e,t])))}),Q(o),T(r,Q)),I},delIndexDefinition:e=>(g(e),I),getStore:n,getIndexIds:o,forEachIndex:e=>r(((t,s)=>e(t,(e=>v(t,e,s))))),forEachSlice:(e,t)=>v(e,t,c(e)),hasIndex:a,hasSlice:(e,t)=>k(c(e),t),getTableId:i,getSliceIds:e=>N(c(e)),getSliceRowIds:(e,t)=>A(O(c(e),t)),addSliceIdsListener:(e,s)=>L(s,t,[e]),addSliceRowIdsListener:(e,t,n)=>L(n,s,[e,t]),delListener:e=>(w(e),I),destroy:f,getListenerStats:()=>({})};return ee(I)})),ie=F([["avg",[(e,t)=>u(e)/t,(e,t,s)=>e+(t-e)/(s+1),(e,t,s)=>e+(e-t)/(s-1),(e,t,s,n)=>e+(t-s)/n]],["max",[e=>v(...e),(e,t)=>v(t,e),(e,t)=>t==e?void 0:e,(e,t,s)=>s==e?void 0:v(t,e)]],["min",[e=>I(...e),(e,t)=>I(t,e),(e,t)=>t==e?void 0:e,(e,t,s)=>s==e?void 0:I(t,e)]],["sum",[e=>u(e),(e,t)=>e+t,(e,t)=>e-t,(e,t,s)=>e-s+t]]]),de=U((e=>{const t=F(),[s,n,o,r,a,i,d,l,c,u]=K(e,m,(e=>isNaN(e)||R(e)||!0===e||!1===e||""===e?void 0:1*e)),[h,g,f]=Y((()=>L)),L={setMetricDefinition:(e,s,n,o,r,a,c)=>{const u=C(n)?[n,r,a,c]:O(ie,n)??O(ie,"sum");return l(e,s,((s,n,o,r,a,l)=>{let c=i(e),h=E(r);const[f,L,p,w]=u;l=l||R(c),D(n,(([e,t])=>{l||(c=R(e)?L?.(c,t,h++):R(t)?p?.(c,e,h--):w?.(c,t,e,h)),l=l||R(c)})),s(),M(r)?c=void 0:l&&(c=f(A(r),E(r))),S(c)||(c=void 0);const v=i(e);c!=v&&(d(e,c),g(t,[e],c,v))}),Q(o,1)),L},delMetricDefinition:e=>(c(e),L),getStore:s,getMetricIds:n,forEachMetric:o,hasMetric:r,getTableId:a,getMetric:i,addMetricListener:(e,s)=>h(s,t,[e]),delListener:e=>(f(e),L),destroy:u,getListenerStats:()=>({})};return ee(L)})),le=(e,t,s,n,o)=>{let r,a=0;const i={load:async s=>{if(2!=a){a=1;const n=await t();R(n)||""==n?e.setTables(s):e.setJson(n),a=0}return i},startAutoLoad:async e=>(i.stopAutoLoad(),await i.load(e),n(i.load),i),stopAutoLoad:()=>(o(),i),save:async()=>(1!=a&&(a=2,await s(e.getJson()),a=0),i),startAutoSave:async()=>(await i.stopAutoSave().save(),r=e.addTablesListener((()=>i.save())),i),stopAutoSave:()=>(T(r,e.delListener),i),getStore:()=>e,destroy:()=>i.stopAutoLoad().stopAutoSave(),getStats:()=>({})};return ee(i)},ce=globalThis.window,ue=(e,t,s)=>{let n;return le(e,(async()=>s.getItem(t)),(async e=>s.setItem(t,e)),(e=>{n=n=>{n.storageArea===s&&n.key===t&&e()},ce.addEventListener("storage",n)}),(()=>{ce.removeEventListener("storage",n),n=void 0}))},he=(e,t)=>ue(e,t,localStorage),ge=(e,t)=>ue(e,t,sessionStorage),fe=(s,n)=>{let o;return le(s,(async()=>{try{return await e.readFile(n,"utf8")}catch{}}),(async t=>{try{await e.writeFile(n,t,"utf8")}catch{}}),(e=>{o=t(n,e)}),(()=>{o?.close(),o=void 0}))},Le=e=>e.headers.get("ETag"),pe=(e,t,s,n)=>{let o,r;return le(e,(async()=>{const e=await fetch(t);return r=Le(e),e.text()}),(async e=>await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:e})),(e=>{o=setInterval((async()=>{const s=await fetch(t,{method:"HEAD"}),n=Le(s);R(r)||R(n)||n==r||(r=n,e())}),1e3*n)}),(()=>{T(o,clearInterval),o=void 0}))},we=U((e=>{const t=F(),s=F(),n=F(),o=F(),[r,a,i,d,l,c,u,h,g,f]=K(e,(()=>[F(),F(),F(),F()]),(e=>R(e)?void 0:e+"")),[L,p,w]=Y((()=>y)),v=(e,t,s)=>T(c(e),(([n,,o])=>{if(!k(o,t)){const r=q();if(l(e)!=S(e))G(r,t);else{let e=t;for(;!R(e)&&!k(r,e);)G(r,e),e=O(n,e)}if(s)return r;P(o,t,r)}return O(o,t)})),I=(e,t)=>T(c(e),(([,,e])=>P(e,t))),S=e=>O(t,e),y={setRelationshipDefinition:(e,r,a,i)=>(P(t,e,a),h(e,r,((t,r)=>{const a=q(),i=q(),d=q(),[l,u]=c(e);D(r,(([t,s],n)=>{R(t)||(G(i,t),T(O(u,t),(e=>{J(e,n),M(e)&&P(u,t)}))),R(s)||(G(i,s),k(u,s)||P(u,s,q()),G(O(u,s),n)),G(a,n),P(l,n,s),j(O(o,e),(t=>{k(v(e,t),n)&&G(d,t)}))})),t(),D(a,(t=>p(s,[e,t]))),D(i,(t=>p(n,[e,t]))),D(d,(t=>{I(e,t),p(o,[e,t])}))}),Q(i)),y),delRelationshipDefinition:e=>(P(t,e),g(e),y),getStore:r,getRelationshipIds:a,forEachRelationship:t=>i((s=>t(s,(t=>e.forEachRow(l(s),t))))),hasRelationship:d,getLocalTableId:l,getRemoteTableId:S,getRemoteRowId:(e,t)=>O(c(e)?.[0],t),getLocalRowIds:(e,t)=>A(O(c(e)?.[1],t)),getLinkedRowIds:(e,t)=>R(c(e))?[t]:A(v(e,t,!0)),addRemoteRowIdListener:(e,t,n)=>L(n,s,[e,t]),addLocalRowIdsListener:(e,t,s)=>L(s,n,[e,t]),addLinkedRowIdsListener:(e,t,s)=>(v(e,t),L(s,o,[e,t])),delListener:e=>(I(...w(e)),y),destroy:f,getListenerStats:()=>({})};return ee(y)})),ve=(e,t,s,n=P)=>{const o=(r=N(e),a=e=>!te(t,e),r.filter(a));var r,a;return c($(t),(n=>s(e,n,t[n]))),c(o,(t=>n(e,t))),e},Ie=e=>{const t=s(e);return b(t)||t==r&&S(e)?t:void 0},Se=(e,t)=>!(R(e)||!(e=>y(e,Z)&&e.constructor==Z)(e)||_(e))&&(ne(e,((s,n)=>{t(s,n)||se(e,n)})),!(e=>g($(e)))(e)),ye=(e,t,s)=>P(e,t,O(e,t)==-s?void 0:s),Re=()=>{let e,t=0,s=0;const n=F(),o=F(),a=F(),d=F(),l=F(),u=F(),h=F(),g=z(q),f=z(q),L=z(),v=z(),I=z(),S=z(),y=z(),[m,E,A,J]=Y((()=>Me)),G=(t,s)=>(!e||k(l,s))&&Se(t,(e=>K(s,e))),K=(e,t,s)=>Se(s?t:U(t,e),((s,n)=>T(Q(e,n,s),(e=>(t[n]=e,!0)),(()=>!1)))),Q=(t,s,n)=>e?T(O(O(l,t),s),(e=>Ie(n)!=e.type?e.default:n)):R(Ie(n))?void 0:n,U=(e,t)=>(T(O(u,t),(t=>ne(t,((t,s)=>{te(e,s)||(e[s]=t)})))),e),V=e=>ve(l,e,((e,t,s)=>{const n={};ve(B(l,t,F()),s,((e,t,s)=>{P(e,t,s),T(s.default,(e=>n[t]=e))})),P(u,t,n)}),((e,t)=>{P(l,t),P(u,t)})),X=e=>ve(h,e,((e,t,s)=>Z(t,s)),((e,t)=>de(t))),Z=(e,t)=>ve(B(h,e,F(),(()=>ue(e,1))),t,((t,s,n)=>$(e,t,s,n)),((t,s)=>le(e,t,s))),$=(e,t,s,n,o)=>ve(B(t,s,F(),(()=>he(e,s,1))),n,((t,n,o)=>_(e,s,t,n,o)),((n,r)=>ce(e,t,s,n,r,o))),_=(e,t,s,n,o)=>{k(s,n)||ge(e,t,n,1);const r=O(s,n);o!==r&&(fe(e,t,n,r),P(s,n,o))},oe=(e,t,s)=>ke((()=>$(e,ie(e),t,s))),re=(e,t,s,n,o)=>T(O(t,s),(t=>_(e,s,t,n,o)),(()=>$(e,t,s,U({[n]:o},e)))),ae=e=>{const s=""+t++;return k(e,s)?ae(e):s},ie=e=>O(h,e)??Z(e,{}),de=e=>Z(e,{}),le=(e,t,s)=>$(e,t,s,{},!0),ce=(e,t,s,n,o,r)=>{const a=O(u,e)?.[o];if(!R(a)&&!r)return _(e,s,n,o,a);const i=t=>{fe(e,s,t,O(n,t)),ge(e,s,t,-1),P(n,t)};R(a)?i(o):j(n,i),M(n)&&(he(e,s,-1),M(P(t,s))&&(ue(e,-1),P(h,e)))},ue=(e,t)=>ye(n,e,t),he=(e,t,s)=>ye(B(o,e,F()),t,s),ge=(e,t,s,n)=>ye(B(B(a,e,F()),t,F()),s,n),fe=(e,t,s,n)=>B(B(B(d,e,F()),t,F()),s,n),Le=(e,t,s)=>{const n=O(O(d,e),t),o=Ce(e,t,s);return k(n,s)?[!0,O(n,s),o]:[!1,o,o]},pe=e=>{const t=M(S[e])&&M(v[e])&&M(f[e]),s=M(y[e])&&M(I[e])&&M(L[e])&&M(g[e]);if(t&&s)return;const r=e?[W(n),W(o,W),W(a,(e=>W(e,W))),W(d,(e=>W(e,W)))]:[n,o,a,d];if(t||(D(r[2],((t,s)=>D(t,((t,n)=>{M(t)||E(S[e],[s,n])})))),D(r[1],((t,s)=>{M(t)||E(v[e],[s])})),M(r[0])||E(f[e])),!s){let t;D(r[3],((s,n)=>{let o;D(s,((s,r)=>{let a;D(s,((s,i)=>{const d=Ce(n,r,i);d!==s&&(E(y[e],[n,r,i],d,s,Le),t=o=a=1)})),a&&E(I[e],[n,r],Le)})),o&&E(L[e],[n],Le)})),t&&E(g[e],[],Le)}},we=()=>H(h,(e=>H(e,H))),Re=()=>N(h),Te=e=>N(O(h,e)),be=(e,t)=>N(O(O(h,e),t)),Ce=(e,t,s)=>O(O(O(h,e),t),s),me=e=>((e=>Se(e,G))(e)&&ke((()=>X(e))),Me),Ee=()=>(ke((()=>X({}))),Me),ke=e=>{if(-1==s)return;s++;const t=e();return s--,0==s&&(s=1,pe(1),s=-1,pe(0),s=0,c([d,n,o,a],x)),t},Me={getTables:we,getTableIds:Re,getTable:e=>H(O(h,e),H),getRowIds:Te,getRow:(e,t)=>H(O(O(h,e),t)),getCellIds:be,getCell:Ce,hasTables:()=>!M(h),hasTable:e=>k(h,e),hasRow:(e,t)=>k(O(h,e),t),hasCell:(e,t,s)=>k(O(O(h,e),t),s),getJson:()=>p(h),getSchemaJson:()=>p(l),setTables:me,setTable:(e,t)=>(G(t,e)&&ke((()=>Z(e,t))),Me),setRow:(e,t,s)=>(K(e,s)&&oe(e,t,s),Me),addRow:(e,t)=>{let s;return K(e,t)&&(s=ae(O(h,e)),oe(e,s,t)),s},setPartialRow:(e,t,s)=>(K(e,s,1)&&ke((()=>{const n=ie(e);ne(s,((s,o)=>re(e,n,t,o,s)))})),Me),setCell:(e,t,s,n)=>(T(Q(e,s,C(n)?n(Ce(e,t,s)):n),(n=>ke((()=>re(e,ie(e),t,s,n))))),Me),setJson:e=>{try{"{}"===e?Ee():me(w(e))}catch{}return Me},setSchema:t=>{if((e=(e=>Se(e,(e=>Se(e,(e=>{if(!Se(e,((e,t)=>i(["type","default"],t))))return!1;const t=e.type;return!(!b(t)&&t!=r||(Ie(e.default)!=t&&se(e,"default"),0))})))))(t))&&(V(t),!M(h))){const e=we();Ee(),me(e)}return Me},delTables:Ee,delTable:e=>(k(h,e)&&ke((()=>de(e))),Me),delRow:(e,t)=>(T(O(h,e),(s=>{k(s,t)&&ke((()=>le(e,s,t)))})),Me),delCell:(e,t,s,n)=>(T(O(h,e),(o=>T(O(o,t),(r=>{k(r,s)&&ke((()=>ce(e,o,t,r,s,n)))})))),Me),delSchema:()=>(V({}),e=!1,Me),transaction:ke,forEachTable:e=>D(h,((t,s)=>e(s,(e=>D(t,((t,s)=>e(s,(e=>j(t,e))))))))),forEachRow:(e,t)=>D(O(h,e),((e,s)=>t(s,(t=>j(e,t))))),forEachCell:(e,t,s)=>j(O(O(h,e),t),s),addTablesListener:(e,t)=>m(e,g[t?1:0]),addTableIdsListener:(e,t)=>m(e,f[t?1:0]),addTableListener:(e,t,s)=>m(t,L[s?1:0],[e]),addRowIdsListener:(e,t,s)=>m(t,v[s?1:0],[e]),addRowListener:(e,t,s,n)=>m(s,I[n?1:0],[e,t]),addCellIdsListener:(e,t,s,n)=>m(s,S[n?1:0],[e,t]),addCellListener:(e,t,s,n,o)=>m(n,y[o?1:0],[e,t,s]),callListener:e=>(J(e,[Re,Te,be],(e=>R(e[2])?[]:[,,].fill(Ce(...e)))),Me),delListener:e=>(A(e),Me),getListenerStats:()=>({})};return ee(Me)};export{oe as createCheckpoints,le as createCustomPersister,fe as createFilePersister,ae as createIndexes,he as createLocalPersister,de as createMetrics,we as createRelationships,pe as createRemotePersister,ge as createSessionPersister,Re as createStore,re as defaultSorter};
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            

SyntaxError: Unexpected token '??='
    at Loader.moduleStrategy (internal/modules/esm/translators.js:145:18)

This is what I did:

  1. npm install tinybase
  2. add following line to package.json: "type": "module"
  3. create index.js with following script:
import {createStore} from 'tinybase';
const store = createStore();
store.setCell('t1', 'r1', 'c1', 'Hello World');
console.log(store.getCell('t1', 'r1', 'c1'));
  1. node index.js

Trying the same with node index.mjs leads to the same result.

Prettier peer dependency causes install errors

Describe the bug

Appreciate your work on this. Ran into a weird issue, tho: The framework I'm using requires Prettier 3 for its internal eslint formatter but for some reason your package requires Prettier 2 as a peer dependency and therefore requires me to install packages with --legacy-peer-deps enabled. If you're not providing a language server to provide live code formatting in this package, could you not require that as a peer dep?

Thanks

❯ npm i
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/prettier
npm ERR!   dev prettier@"^3.0.3" from the root project
npm ERR!   peer prettier@">= 3.0.0" from @electron-toolkit/[email protected]
npm ERR!   node_modules/@electron-toolkit/eslint-config-prettier
npm ERR!     dev @electron-toolkit/eslint-config-prettier@"^1.0.1" from the root project
npm ERR!   3 more (eslint-plugin-prettier, prettier-plugin-svelte, prettier-plugin-tailwindcss)
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peerOptional prettier@"^2.8.8" from [email protected]
npm ERR! node_modules/tinybase
npm ERR!   dev tinybase@"^4.1.3" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/prettier
npm ERR!   peerOptional prettier@"^2.8.8" from [email protected]
npm ERR!   node_modules/tinybase
npm ERR!     dev tinybase@"^4.1.3" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! 
npm ERR! For a full report see:
npm ERR! /Users/daniel/.npm/_logs/2023-08-31T21_11_14_966Z-eresolve-report.txt

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

Create an Electron project using Svelte as your UI framework and add Tinybase.

Expected behavior

To not be forced to choose a linter version by my reactive store?

Screenshots or Videos

No response

Platform

  • OS: MacOS 13.5.1
  • Browser: n/a
  • Version: 4.1.1

Additional context

No response

Ensure that automatically added row IDs always increment

Is your feature request related to a problem? Please describe.
When adding rows with store.addRow() or useRowCallback() after removing rows in reverse order (based on sorted row IDs), the row IDs do not increment continuously. Instead, the first added row ID is one greater than the previous greatest ID, and subsequent row IDs decrease until they form a continuous sequence from 0 to the first incremented ID after row removals.

Describe the solution you'd like
Ensure that the row IDs generated by store.addRow() and useRowCallback() always increment based on the current greatest ID value.

Describe alternatives you've considered
As changing the behavior of store.addRow() and useRowCallback() could affect existing implementations, consider introducing a new method, store.appendRow(), that generates auto-incrementing row IDs based on the current greatest ID value. This new method would provide a more intuitive and expected behavior without disrupting existing code that relies on the current behavior.

A way to handle authentication & user context

Is there any way or example on how to use authentication & user context with TinyBase?

For example in the Todo application. Let's say there are many users & todos in the remote stored database.

A user logins using direct access to the remote database. Then only sync Todo's associated with that user locally.

Unable to run createQueries while using api generate getStoreApi

Describe the bug

I used tinybase getStoreApi <schemaFile> <storeName> <outputDir> which allowed me to set & access data. However, I've been unable to use queries. I get the following error:

TypeError: s is not a function (it is undefined) (sometimes l is not a function)

I have tried both createQueries and useCreateQueries. I noticed that createQueries complains that store is not an instance of Store even though I used the command line above.

Your Example Website or App

https://github.com/fdfontes/tinybase-rn-broken-queries-repro

Steps to Reproduce the Bug or Issue

  1. Run tinybase getStoreApi <schemaFile> <storeName> <outputDir> schema in this case is dbSchema.json
  2. Generates 4 files testStore-ui-react.d.ts, testStore-ui-react.tsx, testStore.d.ts, testStore.ts
  3. Use createQueries in App.js

Expected behavior

I expected to be able to set up queries.

Screenshots or Videos

Simulator Screen Shot - iPhone 14 Plus - 2023-08-01 at 09 10 36

Platform

  • OS: macOS
  • Using Expo + React Native

Additional context

I tried to include createQueries and useCreateQueries in my UI files. The documentation isn't super clear on how queries work when using the auto generated UI. Perhaps I'm not understanding some element of the implementation.

Better types for `setValues` & `useStore`

Currently, setValues & setValue will ignore updating the store if a property doesn't match the set schema. This is great.

But would be very helpful to also have some info on the type side when setting the values (or row/cell). Would also be nice to get the schema types from useStore.

Missing folders on first builds

$ npm run compileDocs v14.16.0 01/16/22 7:26AM

[email protected] compileDocs
gulp compileDocs

[07:26:49] Using gulpfile
[07:26:49] Starting 'compileDocs'...
[07:26:50] 'compileDocs' errored after 1.1 s
[07:26:50] Error: ENOENT: no such file or directory, scandir 'lib/umd'
at readdirSync (fs.js:1021:3)

Use a prisma schema with TinyBase so you can sync with a hosted postgres db

I'd like to have a single prisma schema that can be used with the inside setTablesSchema().

An example schema.primsa file:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "postgres"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

// --------------------------------------

model User {
  id             Int          @id @default(cuid())
  createdAt      DateTime     @default(now())
  updatedAt      DateTime     @updatedAt
  email          String       @unique
  hashedPassword String?
  role           String       @default("USER")
  avatar         Json?
  tokens         Token[]
  sessions       Session[]
  todos            Todo[]
}

model Session {
  id                 Int       @id @default(cuid())
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt
  expiresAt          DateTime?
  handle             String    @unique
  hashedSessionToken String?
  antiCSRFToken      String?
  publicData         String?
  privateData        String?
  user   User? @relation(fields: [userId], references: [id])
  userId Int?
}

model Token {
  id          Int       @id @default(cuid())
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  hashedToken String
  lastFour String?
  type        TokenType
  expiresAt   DateTime
  sentTo      String?
  user   User? @relation(fields: [userId], references: [id])
  userId Int?
  @@unique([hashedToken, type])
}

enum TokenType {
  RESET_PASSWORD
  INVITE_TOKEN
  PUBLIC_KEY
  SECRET_KEY
}

model Todo {
  id         String   @id @default(cuid())
  createdAt  DateTime @default(now())
  modifiedAt DateTime @default(now())
  name String
  slug String @unique
  user   User @relation(fields: [userId], references: [id])
  userId Int
}

Notice the @unqiue & @relation helpers

Many-to-many support

Distant wishlist, but how could a cell point to multiple other cells?

Via @founderYonz

Allow null values in cells

When creating a new row it is impossible to store null values in a cell. It will be just dropped.
In my app, I need to check specifically for nulls

    store.setRow('files', 'test_id', {
      name: 'Test file',
      folder_id: null,
    })

    console.log(store.getRow('files', 'test_id')) // {name: 'Test file'}

IndexedDB persister

Is your feature request related to a problem? Please describe.
localStorage is limited to 5MB in size.

Describe the solution you'd like
IndexedDB is a browser API for client-side storage of significant amounts of structured data. Since it's a database, I would think it could be a nice complement to TinyBase. Ideally, every change to TinyBase would be incrementally pushed to IndexedDB (maybe even in a Web Worker to unblock the main thread).

Do you know if anyone has explored this option? Are there any reasons this might not be a good idea?

Unexpected behavior with ids parsed on local storage.

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

Assume you set a table like this:

store.setTable('tableName', { parseInt('123'): { 'attr': 1, ...otherAttrs }})

This is wrong because rowId should not be a number but stick with me.

If later you set a cell in this table and rowId like.

store.setCell('tableName', parseInt('123'), 'attr', 2);

This will override the localStorage instance of this rowId to only { 'attr': 2 }. There's no other attributes that we initially set.
I'm assuming this happens because when we set the table with a rowId of type number, internally this number is casted into a string.
When on setCell we offer the same rowId of type number with which we set the table, the rowId is not a string anymore, thus the instance cannot be found and an empty object is returned, which we then populate with the value of setCell. And ultimately, when the save operations happens, the rowId of type number is again casted as string which then overrides the localStorage instance

Describe the solution you'd like
A clear and concise description of what you want to happen.

Just a warning would be nice whenever we're setting rowIds as number, or in a more consistent fashion with the error handling of tinybase until now, we could have it just silently fail the operation of setting a cell if the rowId provided is a number.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Error using tinybase in electron.js app

Describe the bug

I'm currently trying to use tinybase in an electon.js personal project but keep on getting an import error saying
Error [ERR_REQUIRE_ESM]: require() of ES Module ...\tinybase.js from .....\dataBase.js not supported. Instead change the require of tinybase.js in ...\dataBase.js to a dynamic import() which is available in all CommonJS modules.

dataBase.js is the file where I'm trying to use tinybase

Your Example Website or App

https://github.com/uwemneku/electron-quick-start/blob/master/main.js

Steps to Reproduce the Bug or Issue

  1. Clone this repo
  2. Start the app npm start

Expected behavior

As a user, I expect the app to start without any issues

Screenshots or Videos

image
image

Platform

  • OS: [Windows]
  • Browser: [Chronium]

Additional context

No response

Tinybase not compatible with metro in react-native

Describe the bug

Hello, I'm trying to use tinybase for a project with react-native and I keep getting this error:

While trying to resolve module `tinybase` from file `---path---\App.tsx`, the package `--path--\node_modules\tinybase\package.json` was successfully found. However, this package itself specifies a `main` module field that could not be resolved (`--path---\node_modules\tinybase\index`. Indeed, none of these files exist.:.....

In my opinion, looks like metro has issues with the way the package is exported in package.json and cannot resolve the correct path.

Your Example Website or App

React native is not an option in stackblitz

Steps to Reproduce the Bug or Issue

  1. Initialize a react native app
  2. Install tinybase
  3. Import and interact with tinybase

Expected behavior

I would expect tinybase to work similar to how it does in a react or next app, with webpack.

Screenshots or Videos

No response

Platform

  • OS: [macOS, Windows, Linux]
  • Browser: [Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response

[doc] [typo] Using Context doc page guides/building-uis/using-context/ uses too when it should use to

Describe the bug

The following paragraph uses "too" when it should use "to". See bold too:

To help with this, the Provider component lets you specify a Store that all the hooks and components will bind too automatically. Simply provide the Store in the store prop, and it will be used by default. Notice how the store variable is not referenced in the child Pane component here, for example:

To Reproduce
Steps to reproduce the behavior:

  1. Go to 'guides/building-uis/using-context/
  2. Click on 'guides/building-uis/using-context/
  3. Scroll down to "Using Context", 3pm rd paragraph
  4. See typo

Expected behavior
Should stay " will bind to automatically"

Screenshots

Screenshot 2022-01-15 at 12 11 25

tinybase.org crashes Brave on iOS

Describe the bug

As described

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

No response

Expected behavior

No response

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response

Add errors

First off, thank you for a great library. Using a client-side relational database is, in my opinion, the clear future for large frontend applications. However, after trying Tinybase out there is a key feature that I find myself really missing: errors.

Tinybase has cell types and default values. That is, I can enforce my data never has the wrong type, and I can substitute a default value when applicable, but I can't throw an error when my app tries to add invalid data to my store. Everything fails silently. This is fine in some circumstances, but is very much not in others.

I'd like a way to say, this cell is required. This should throw something akin to a Postgres NotNullConstraint error. This also goes for foreign key constraints, check constraints, etc.

In my opinion there should be no requirement to use these constraints nor even a schema, but they should be available.

For context, my current work around is to set a unique default value for fields which I require which is used no-where else in my application. I then use listeners to watch for this value and, if they find it throw an error. This is very brittle. I'd much prefer a built-in solution.

React native metro code target

Describe the bug

Hello, so I'm configuring eas update in one of my apps in react-native where I'm encountering a problem.

[expo-cli] SyntaxError: node_modules\tinybase\lib\indexes.js: Unexpected token: operator (?) in file node_modules\tinybase\lib\indexes.js at 224:11
[expo-cli] Error: Unexpected token: operator (?) in file node_modules\tinybase\lib\indexes.js at 224:11
[expo-cli]     at minifyCode (--path--\node_modules\metro-transform-worker\src\index.js:101:13)
[expo-cli]     at transformJS (--path--\node_modules\metro-transform-worker\src\index.js:319:28)
[expo-cli]     at transformJSWithBabel (--path--\node_modules\metro-transform-worker\src\index.js:410:16)
[expo-cli]     at processTicksAndRejections (node:internal/process/task_queues:96:5)
[expo-cli]     at async Object.transform (--path--\node_modules\metro-transform-worker\src\index.js:571:12)

It seems that the transpiling target for tinybase is too high for metro which makes metro fail when it encounters new syntax like optional chaining etc.

It would be nice if the target was set to something that includes these features.

I have been debugging the problem for a while and it appears to me that adding this would help:

https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining.

I patched tinybase from node_modules with yarn by importing things from "lib/debug" and literally replacing lib/debug/*.js with the babel ie9 transpiled versions of those files and it all works, but it would be nice to have this from the package as others will probably encounter this problem in the future too.

Thank you

Your Example Website or App

No stackblitz for react-native

Steps to Reproduce the Bug or Issue

.

Expected behavior

.

Screenshots or Videos

No response

Platform

  • OS: [Windows]
  • Tinybase Version: [1.3.2]

Additional context

No response

Web Dashboard / Admin UI?

Is your feature request related to a problem? Please describe.
When experimenting/debugging, I like having some sort of UI for my databases, such as https://github.com/sqlitebrowser/sqlitebrowser. For TinyBase, I think it'd make sense to have a simple JS web UI that users can interact with their data from.

Describe the solution you'd like
Something similar to deployd's web dashboard, which can be used for basic CRUD operations.

Describe alternatives you've considered
N/A

Additional context
deployd was a cool API builder that is no longer actively maintained. https://github.com/deployd/deployd

Enable Github Sponsors for TinyBase

Sorry for the deluge of issues! We've been prototyping with TinyBase over the last couple weeks and have been blown away by how much you've achieved so rapidly.

I'm not sure if financial gain is helpful or motivating for you but I'd love to be able to make a monthly contribution to the continued development of this incredible project. I could only offer a smaller personal donation to start but if we end up moving forward with TinyBase at my $DAY_JOB I'd definitely push for some kind of larger sponsorship as well

Ability to create Metrics on top of Indices

Is your feature request related to a problem? Please describe.
I'm working on a local app that conceptually has a list of items pertaining to a certain parent project. For simplicity, the schema could be:

{
  project: {
     id: string
  },
  item: {
    id: string,
    projectId: string,
    isComplete: boolean
  }
}

I'd like to be able to have a metric that allows me to calculate percentage calculation for each project based on its related items

Describe the solution you'd like
It seems like it would make sense to be able to define a metric definition that targets a specific index instead of the overall table.

Describe alternatives you've considered
We can definitely recreate the aggregate completion calculation using other TinyBase primitives but it feels like there's an opportunity for an elegant addition to the core API

Additional context
Add any other context or screenshots about the feature request here.

Documentation Navigation Challenges

Hey, I've been reading through all of the TinyBase docs and loving what I'm seeing. I ran into a few navigation challenges and thought I'd share them (instead of creating a speculative PR that might not match the style you're going for).

I wanted to read TinyBase's docs sequentially to best understand it. However, I ran into a few issues:

Sidebar is Hidden at Above Mobile Sizes

On a portrait iPad, I had more than enough room to see the sidebar -- but it had been hidden by a breakpoint. It would still be useful to show the navigation until a smaller breakpoint, even if it requires the user to scroll for full visibility of the text.

Headings in Top Level Guide Pages Don't Look Clickable

Without underlines or some other distinguishing mark, headings like "Getting Started" on this page don't look clickable. Even more challenging when sidebar is hidden, now that the headings are the only way of navigating into a section.

Finding the Next Guide

Some pages lack links to the next guide, like the Creating A Store page. Others make it difficult to tell which word will continue to the next page, like the Using Indexes page.

Because of how the guides are structured, I'd love a clear previous and next guide button at the bottom of the page. I love that the guides are written assuming the user is reading them sequentially, it'd be great to be able to browse sequentially as well.


Really excited to give TinyBase a try! I feel like it's solving a problem that will become more and more common as apps grow larger and more complex. Also kudos for having so many demos at different levels of complexity.

Support an array of sliceId for setIndexDefinition?

Is your feature request related to a problem? Please describe.
We were trying to create indexes for events object by date. So that if we look for a specific date then it will return the collection of events that cover that given date.
Event { start: Date, end: Date, ...}

indexes.setIndexDefinition( 
    'eventsByDate', 
    'events',
    (getCell) =>  ['2022-09-19', '2022-09-20'] // eg. return array of ISO date from this code: eachDayOfInterval(getCell('from'), getCell('to')).map((date) => formatISO(date))
);

indexes.getSliceRowIds('eventsByDate', '2022-09-19') // return events that cover this date 
indexes.getSliceRowIds('eventsByDate', '2022-09-20')  // return events that cover this date  

Describe the solution you'd like
I think if the setIndexDefinition could support having sliceId array to index rows then it's more flexible.

Describe alternatives you've considered
We are not sure of the best way to solve this with Tinybase without manually indexing rows by listening to the row-adding event.

Additional context
N/A

Nested and related data?

Sorry to open this as an issue but I noticed you've previously said you don't get notifications for Discussions.

Has there been any further movement on supporting one->many queries? Or a pattern I should be following?

_Originally posted in #18

Ability to assign rowId when adding a new row

Forgive me if this is covered somewhere, but looking through the docs and playing around with examples using store.addRow() or useRowCallback() it doesn't seem like there is a way to provide our own ID and we must rely on receiving one that was assigned automatically by tinybase

Describe the solution you'd like
It would be great if there was an optional way to add your own ID for a row.

  • Explicitly using the methods above
  • Maybe as part of the table schema definition we could add a factory function (eg generating a uuid) that could be used for ID generation?

Tinybase fails to build with Browserify

Describe the bug

When adding an import "tinybase" or require("tinybase") in a file that is then passed to Browserify for buliding, it fails with the error

Error: Can't walk dependency graph: Cannot find module 'tinybase' 

Perhaps it is related to bundler requirements, but I build by app with many other packages and they all work just fine.

Your Example Website or App

https://github.com/shaneosullivan/tinybaseRepro

Steps to Reproduce the Bug or Issue

1: Go to https://github.com/shaneosullivan/tinybaseRepro and check out the project
2: Run npm install
3: Run npm run repro

Expected behavior

It should successfully build the file output.js without error

Screenshots or Videos

No response

Platform

  • OS: macOS
  • Browser: Terminal
  • Version: 1.3.3

Additional context

No response

Getters return empty on first render

Hello, thank you for your work, this is a really nice project and I am using it.

I want to report what seems to me as a bug. The getter methods return empty objects on first render instead of the expected behavior which is to return the storage content.

I am persisting to local storage and on the first render I would expect to get the saved data but I get an empty object.
This only happens with the store methods, because when I manually check the local storage I can access the data.

To visualize this, here is some simple code which could serve for reproducing the bug as well.

useEffect(() => {
      const table = store.getTable('products');
      const table2 = localStorage.getItem('products/store');
      
      console.log(table); // returns empty { }
      console.log(table2); // returns data
}, [])

It works just fine on subsequent renders though and table returns the data normally then.

Thank you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.