LDflex / LDflex

A JavaScript DSL for querying Linked Data on the Web
https://ldflex.github.io/LDflex/
MIT License
176 stars 18 forks source link

Write support #5

Closed rubensworks closed 5 years ago

rubensworks commented 5 years ago

This is a proposal for adding write-support to LDflex.

It requires three new handles:

Addition

This requires a straightforward handler that intercepts the add and addAll functions that take one mandatory argument.

Adding a single value:

await user.friends.add('https://rubensworks.solid.community/profile/card#me')

SPARQL:

INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <https://rubensworks.solid.community/profile/card#me>.
}

Adding the first of multiple values:

await user.friends.add(user.friends.friends)

SPARQL:

INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows
      <http://example.org/A>.
}

Adding multiple values:

for await (const addedFriend of user.friends.add(user.friends.friends)) {
    console.log(addedFriend)
}

SPARQL:

INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/A>.
}
INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/B>.
}
INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/C>.
}

Adding multiple values in bulk:

await user.friends.addAll(user.friends.friends)

SPARQL:

INSERT DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows
      <http://example.org/A>,
      <http://example.org/B>,
      <http://example.org/C>.
}

Note: We can not trap = via the proxy (set), as this requires a synchronous operation, and you can not play around with return values.

Deletion

This requires a handler that intercepts the delete and deleteAll functions that take one optional argument.

Deleting a single value:

await user.friends.delete('https://rubensworks.solid.community/profile/card#me')

SPARQL:

DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <https://rubensworks.solid.community/profile/card#me>.
}

Deleting the first of a list of values:

await user.friends.delete(user.friends.friends)

SPARQL:

DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows
      <http://example.org/A>.
}

(maybe this can be done with a DELETE WHERE?)

Deleting a list of values:

for await (const deletedFriend of user.friends.add(user.friends.friends)) {
    console.log(deletedFriend)
}

SPARQL:

DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/A>,
}
DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/B>,
}
DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows <http://example.org/C>,
}

Deleting a list of values in bulk:

await user.friends.deleteAll(user.friends.friends)

SPARQL:

DELETE DATA
{ 
  <https://rubensworks.solid.community/profile/card#me> foaf:knows
      <http://example.org/A>,
      <http://example.org/B>,
      <http://example.org/C>.
}

Deleting first or all possible values:

await user.friends.delete()
await user.friends.deleteAll()

Internally, this corresponds to await user.friends.delete(user.friends) or await user.friends.deleteAll(user.friends), because SPARQL DELETE does not allow blank nodes or variables

Note: We can not trap the delete operator via the proxy (deleteProperty), as this requires a synchronous operation, and you can not play around with return values.

Update

This corresponds to a delete followed by an insert. This is merely convenience functionality that combines deletion and addition (optimizable with a combined SPARQL query: https://www.w3.org/TR/2013/REC-sparql11-update-20130321/#deleteInsert). This requires a handler for set and setAll that take one mandatory argument.

Updating a single value:

await user.name.set('Ruben')

Translation:

await user.name.delete() # Delete a single value
await user.name.add('Ruben') # Add a single value

Updating the first of multiple value:

await user.name.set(user.friends.name)

Translation:

await user.name.delete()
await user.name.add(user.friends.name)

Updating multiple values:

for await (const newName of user.name.set(user.friends.name)) {
    console.log(newName)
}

SPARQL:

await user.name.deleteAll()
await user.name.add(name0) # add calls are separated by for-await iterations.
await user.name.add(name1)
await user.name.add(name2)
...

Updating multiple values in bulk:

await user.name.setAll(user.friends.name)

SPARQL:

await user.name.deleteAll()
await user.name.addAll(user.friends.name)
RubenVerborgh commented 5 years ago

Good stuff, great as starting point for discussions.

Adding the first of multiple values:

await user.friends.add(user.friends.friends)

I don't find this intuitive; to me, the above syntax should be for adding all friends. For adding one friend (not the first BTW, no ordering), I'd say:

await user.friends.add(user.friends.friends.random)

So I'd be against for await and addAll.

Deleting the first of a list of values:

await user.friends.delete(user.friends.friends)

Deleting a list of values in bulk:

await user.friends.deleteAll(user.friends.friends)

I find the duplication confusing. I would propose

await user.friends.random().delete()
await user.friends.delete() // deletes all

Too bad that we cannot do

await delete user.friends

Because the delete trap is supposed to return a Boolean. That said, we might be able to do it if that Boolean also is a thenable! (untested)

Updating a single value:

await user.name.set('Ruben')

Can we also wire up the following alternative?

user.name = 'Ruben'

Also, there is no such cases as "single value", because anything might have multiple.

How about

user.name.set('Ruben') // removes all names and add one 'Ruben'
user.name.replace('ruben', 'Ruben') // remove all matching names and add one 'Ruben'
rubensworks commented 5 years ago

Adding the first of multiple values: await user.friends.add(user.friends.friends)

I don't find this intuitive; to me, the above syntax should be for adding all friends. For adding one >friend (not the first BTW, no ordering), I'd say: await user.friends.add(user.friends.friends.random)

Sure, that makes sense as well.

On the other hand, this may be confusing, as await user.friends.friends returns only a single value. Perhaps await user.friends.friends.random or await user.friends.friends.random() should be a thing then as well?

So I'd be against for await and addAll.

Agreed. I was just writing out the possibilities :-)

What might be useful though, is to implement addAll like this:

await user.friends.addAll([ 'uri-a', 'uri-b', ... ])

The advantage of this is that a single bulk SPARQL query can be created, instead of having to send one for each addition separately.

await user.friends.random().delete() await user.friends.delete() // deletes all

👍

Too bad that we cannot do await delete user.friends Because the delete trap is supposed to return a Boolean. That said, we might be able to do it if that Boolean also is a thenable! (untested)

Hmm, maybe, I'll look into that.

Can we also wire up the following alternative? user.name = 'Ruben'

We have the same problem here as with delete. The return value of proxy's set should be a boolean value. Since a regular assignment returns its assigned value, we won't be able to promisify this.

We could definitely support this, but any rejections would be uncaught.

(A babel plugin might be possible here as well though)

How about user.name.set('Ruben') // removes all names and add one 'Ruben' user.name.replace('ruben', 'Ruben') // remove all matching names and add one 'Ruben'

👍

RubenVerborgh commented 5 years ago

On the other hand, this may be confusing, as await user.friends.friends returns only a single value.

Yes, but there's no await in front of it when adding, so I think we're good.

Perhaps await user.friends.friends.random or await user.friends.friends.random() should be a thing then as well?

Definitely; I suggest the former. The difference being that a for await over random would also only give at most one element.

What might be useful though, is to implement addAll like this:

await user.friends.addAll([ 'uri-a', 'uri-b', ... ])

Possibly, or we could just splat it:

await user.friends.add(...array)

Can we also wire up the following alternative? user.name = 'Ruben'

We have the same problem here as with delete. The return value of proxy's set should be a boolean value. Since a regular assignment returns its assigned value, we won't be able to promisify this.

We could definitely support this, but any rejections would be uncaught.

You just gave me a really interesting idea.

Recall how Node has this thing about uncaught Promise rejections.

What if we similarly allow such sync calls, which indeed have not completed yet, but give the option to manually sync? Example:

// Do data things
data.user.name = 'Ruben';
data.user.brother.name = 'Niels';
data.user.sister.name = 'Muriel';
delete data.user.daughter;

// Wait until all of the above is done
await data.pending;

Above, data.pending is a Promise to an array of results for each of the currently pending operations.

You're free to await or not await it, knowing that if you don't, you might miss completion and/or rejection (and rejections would become global rejection errors).

Additionally, this gives us the possibility of bundling setters internally, and writing them all at once.

Note that, upon accessing data.pending, all of the currently pending promises are returned and then forgotten. For example:

data.user.name = 'Ruben';
data.user.brother.name = 'Niels';
data.user.sister.name = 'Muriel';
const p1 = data.pending;
const p2 = data.pending;
console.log(await p2); // empty array
console.log(await p1); // three items

This allows explicit scoping of pending promises.

rubensworks commented 5 years ago

What if we similarly allow such sync calls, which indeed have not completed yet, but give the option to manually sync?

That's an interesting idea to enable batching. The await data.pending does feel a bit like it exposes too much of the internals IMO. We can definitely use this internally, but I think it would be better if the user shouldn't know about this.

What if we enable 2 modes of mutation operations?

  1. non-batched operations: like your example above, operations are started the moment they are executed, but they are not grouped.
  2. batched operations: a new construct that allows operations to be grouped, and started from the moment that this construct is closed.

non-batched operations

// Do data things non-batched
data.user.name = 'Ruben';
data.user.brother.name = 'Niels';
data.user.sister.name = 'Muriel';
delete data.user.daughter;

// Optionally wait until all of the above is done, but we should discourage this?
await data.pending;

batched operations

// Do data things batched
data.batch((batch) -> {
  batch.user.name = 'Ruben';
  batch.user.brother.name = 'Niels';
  batch.user.sister.name = 'Muriel';
  delete batch.user.daughter;
});

or:

// Do data things batched
const batch = data.batch();
batch.user.name = 'Ruben';
batch.user.brother.name = 'Niels';
batch.user.sister.name = 'Muriel';
delete batch.user.daughter;

The second option might be a bit more complex to implement, but should be doable using weak references on batch with WeakMap.

upon accessing data.pending, all of the currently pending promises are returned and then forgotten.

Are you sure we want this? This would break use cases where the pending promise should be used to block multiple things. I don't think there's a problem with data.pending just always returning a Promise.all over all pending promises at the moment is was called. If the operations would already be finished at the time the promise is await-ed, the promise will already be resolved, so there's shouldn't be a problem there.

rubensworks commented 5 years ago

I've written everything down in a separate gist that I will update once we decide new things: https://gist.github.com/rubensworks/1123742b048bd2e82624c713a024dcab

I think we agree on everything already, except for perhaps the syntactical sugar and batching. But those build on top of the previous ones, so I can already go ahead an start implementing those.

RubenVerborgh commented 5 years ago

But those build on top of the previous ones, so I can already go ahead an start implementing those.

Exactly!

I very much like your suggestion with the batch function wrapper; it resembles the React state update pattern, and also gives the possibility to do transactions etc. Let's go that way once the core is there.

rubensworks commented 5 years ago

Too bad that we cannot do await delete user.friends Because the delete trap is supposed to return a Boolean. That said, we might be able to do it if that Boolean also is a thenable! (untested)

Just did some testing, and the set and delete traps unfortunately don't allow you to customize the return value of the operation. So we can probably only use them in the context of batching.

rubensworks commented 5 years ago

Closing this issue, since write support has been fully implemented in #6.

Created a separate issue for batched operations: #8.

angelo-v commented 5 years ago

Stumbled upon this by accident. Would be great to have it in the official documentation

angelo-v commented 5 years ago

Ok, I see there is https://github.com/RubenVerborgh/LDflex/issues/10 already :)