Write Data
You must define a collection before writing data to Polybase DB.
You can view our example apps Simple CRUD and Polybase DB Social to see it working in action.
Creating a record
You can create a new record for a collection by calling .create([arg1, arg2, etc])
on your collection (as defined by the collection schema constructor
function).
This will call the constructor
function of your collection, and
create a new record (as long as the id
of the record does not already exist).
import { Polybase } from "@polybase/client"
const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("City");
async function createRecord () {
// .create(args) args array is defined by the constructor fn
const recordData = await collectionReference.create([
"new-york",
"New York",
db.collection("Person").record("johnbmahone")
]);
}
The data returned would be:
{
"block": { "hash": "...", ... },
"data": {
"id": "new-york",
"name": "New York",
"mayor": { "collectionId": "your-namespace/Person", "id": "johnbmahone" }
}
}
And your collection schema might look like:
@public
collection Person {
id: name;
...
}
@public
collection City {
id: string;
name: string;
mayor: Person;
constructor (id: string, name: string, mayor: Person) {
this.id = id;
this.name = name;
this.mayor = mayor;
}
}
Updating a record
You can update collection data using the collection methods defined in the collection.
import { Polybase } from "@polybase/client"
const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("City");
async function updateRecord () {
// .create(functionName, args) args array is defined by the updateName fn in collection schema
const recordData = await collectionReference
.record("new-york")
.call("updateMayor", [db.collection("Person").record("johnbmahone")]);
}
Would require the following collection schema:
@public
collection Person {
id: name;
...
}
@public
collection City {
id: string;
name: string;
mayor: Person;
constructor (id: string, name: string, mayor: Person) {
this.id = id;
this.name = name;
this.mayor = mayor;
}
updateMayor(mayor: Person) {
this.mayor = mayor;
}
}
Permissions
You can control who is allowed to make changes to a record by using the
ctx.publicKey
which is set to the publicKey of the user that signed the
request.
A common use case is to store the ctx.publicKey
of the user who created the
collection, and then use this to determine if the change is valid.
For example:
collection CollectionName () {
id: string;
name: string;
publicKey: PublicKey;
constructor (id: string, name: string) {
this.id = id;
this.name = name;
this.publicKey = ctx.publicKey;
}
setName (name: string) {
if (this.publicKey != ctx.publicKey) {
throw error('invalid public key');
}
this.name = name;
}
}
Signing Requests
To sign requests from the client, you must define a signer function that will be called for every request.
import { Polybase } from "@polybase/client"
import * as eth from "@polybase/eth"
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" })
// Add signer fn
db.signer(async (data: string) => {
// A permission dialog will be presented to the user
const accounts = await eth.requestAccounts();
// If there is more than one account, you may wish to ask the user which
// account they would like to use
const account = accounts[0];
const sig = await eth.sign(data, account);
return { h: "eth-personal-sign", sig };
})
If you want to skip signing for specific requests you can return null
to skip
the signing process.
Using wallet through an extension
You can use the signing process provided by a user's browser extension (e.g. Metamask). Using this approach, every write must be individually approved by the user (i.e. a dialog will appear for them to approve), which may not be an optimal user experience.
import { Polybase } from "@polybase/client"
import * as eth from "@polybase/eth"
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" })
async function createWalletUsingExtension () {
// Set data with publicKey
await db
.collection("user-info")
.record("user-1")
.call("collectionFn", ["Awesome User"])
// Add signer fn
db.signer(async (data: string) => {
// A permission dialog will be presented to the user
const accounts = await eth.requestAccounts()
// If there is more than one account, you may wish to ask the user which
// account they would like to use
const account = accounts[0]
const sig = await eth.sign(data, account)
return { h: "eth-personal-sign", sig }
})
}
Creating your own wallet
To improve the user experience, you can create your own app-owned wallet (public/private key pair), allowing you to sign requests without asking the user every time. The wallet/privateKey can then be encrypted using the browser extension, and stored locally or any other storage system.
That means you only need to ask the user a single time for permission to decrypt the private key, and then use that private key to sign every subsequent request.
import { Polybase } from '@polybase/client'
import { ethPersonalSign } from '@polybase/eth'
import { secp256k1 } from '@polybase/util'
async function createWallet () {
// First time the user signs up to your dapp (generate key AKA wallet)
const privateKey = await secp256k1.generatePrivateKey()
const db = new Polybase({ defaultNamespace: "your-namespace" })
// Add data with publicKey that will own the record
db.collection('your-namespace/City').record('london').call("set", [{
name: 'London',
country: 'UK',
}])
// Add signer fn
db.signer(async (data: string) => {
return { h: 'eth-personal-sign', sig: ethPersonalSign(privateKey, data) }
})
return { privateKey, publicKey: secp256k1.getPublicKey64(privateKey) }
}
Encrypt data
All data on Polybase DB is publicly accessible (like a blockchain). Therefore it is important to ensure private information is encrypted. You can encrypt data however you like, including using a user wallet's public key.
Using wallet through an extension
You can send a request to Metamask (or other compatible wallet) to obtain an encryption key which can be used to encrypt values. However, decryption is only possible by sending a secondary request to the wallet (which results in the permission popup) to ask for permission for each value to be decrypted.
This can result in a poor user experience if there are a number of different values to decrypt, as the user will have to give permission separately for each value.
Here is an example:
import { Polybase } from "@polybase/client";
import * as eth from "@polybase/eth";
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
async function encryptUsingExtension () {
// A permission dialog will be presented to the user
const accounts = await eth.requestAccounts();
// If there is more than one account, you may wish to ask the user which
// account they would like to use
const account = accounts[0];
// A permission dialog will be presented to the user
const encryptedValue = await eth.encrypt("top secret info", account);
await db
.collection("user-info")
.record("user-1")
.call("functionName", [encryptedValue]);
// Later...
// Get the data from Polybase DB as normal
const userData = await db.collection("user-info").record("user-1").get();
// Get the encrypted value
const encryptedValueForUser = userData.data.encryptedValue;
// A permission dialog will be presented to the user every time this method
// is called
const decryptedValue = await eth.decrypt(encryptedValueForUser, account);
}
Creating your own wallet
You can create your own app-owned wallet (public/private key) allowing you to encrypt/decrypt values without having to ask the user for explicit permission each time. The private key should be encrypted using a users existing wallet or a password, but rather than encrypting a specific value, you encrypt the new private key.
That means you only need to ask the user a single time for permission to decrypt the private key, and then use that private key to decrypt all other values. It is then your responsibility to ensure that the encrypted private key is kept safe, which could be either stored locally in browser storage or in Polybase DB.
Here is an example:
import { Polybase } from "@polybase/client";
import { secp256k1 } from "@polybase/util";
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
async function createWallet () {
// First time the user signs up to your dapp (generate key pair AKA wallet)
const privateKey = secp256k1.generatePrivateKey()
const publicKey = secp256k1.getPublicKey64(privateKey)
// Encrypted value will be returned as a hex string 0x...
const encryptedValueAsHexStr = await secp256k1.asymmetricEncryptToEncoding(
publicKey,
"top secret info"
);
await db
.collection("user-info")
.record("user-1")
.call("set", ["Awesome User", encryptedValueAsHexStr]);
// Later...
// Get the data from Polybase DB as normal
const userData = await db.collection("user-info").record("user-1").get();
// Get the encrypted value
const encryptedValue = userData.data.secretInfo;
// Original value returned
const decryptedValue = secp256k1.asymmetricDecryptFromEncoding(
privateKey,
encryptedValue
);
}
There are a number of different places to store the encrypted private key that lead to different trade offs. You must find a tradeoff that is acceptable for your specific application.
Store locally
You could store the encrypted private key locally on the browser device (e.g. in local storage). The tradeoff is that the private key could easily become lost if the user resets their browser (which would make all data unavailable and there would be no recovery method), and it would be difficult for users to work across devices.
Store on Polybase DB
You could store the encrypted private key on Polybase DB, this allow the encrypted private key to obtained by the user and then decrypted on any device.
Multi User Encryption
It's often useful to allow multiple users to decrypt and view data stored in Polybase DB.
To do this, you should:
- Create a symmetric encryption key and encrypt the data with that key
- Encrypt the symmetric encryption key with the public key of each user who should have read access
Example
The following shows an example of how you might create a Google Forms product.
Overview
Collection Schema
@public
collection User {
id: string;
publicKey: PublicKey;
constructor () {
this.id = ctx.publicKey.toHex();
this.publicKey = ctx.publicKey;
}
}
@public
collection Response {
id: string;
data: string;
constructor (id: string, data: string) {
this.id = id;
this.data = data;
}
}
@public
collection ResponseUser {
id: string;
userId: string;
responseId: string;
// symmetric key encrypted with users public key
encryptedSymmetricKey: string;
constructor (userId: string, responseId: string, encryptedSymmetricKey: string) {
this.id = userId + "-" + responseId;
this.userId = userId;
this.responseId = responseId;
this.encryptedSymmetricKey = encryptedSymmetricKey;
}
}
New Form Response Code
import { Polybase } from "@polybase/client";
import { secp256k1, aescbc, encodeToString } from "@polybase/util";
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
// Obtain form response
const data = JSON.stringify({ doYouLikePenguins: true });
// Upload form response
addFormResponse(data);
async function addFormResponse(data: string) {
// Generate a symmetric encryption key (to encrypt form response)
const symmetricKey = await aescbc.generateSecretKey();
const symmetricEncryptedStr = await aescbc.symmetricEncryptToEncoding(
symmetricKey,
data,
"base64"
);
// Store the encrypted response
await db.collection("Response").create(["resp-1", symmetricEncryptedStr]);
// Get list of users who will have access to the form response
// (you will probably want to use a .where() here to filter for a subset
// of users)
const users = await db.collection("Users").get();
// Encrypt with each users public key and store
const p = users.data.map(async (user) => {
const { id: userId, publicKey } = user.data;
// Encrypt the symmetric key
const encryptedSymmetricKeyWithUsersPublicKeyStr =
secp256k1.asymmetricEncryptToEncoding(
publicKey,
encodeToString(symmetricKey, "base64"),
"base64"
);
// Store the encrypted symmetric key
return db
.collection("ResponseUser")
.create([userId, "resp-1", encryptedSymmetricKeyWithUsersPublicKeyStr]);
});
await Promise.all(p);
}
Read a response for a user who was given permission
import { Polybase } from "@polybase/client";
import { secp256k1, aescbc, decodeFromString } from "@polybase/util";
// Init
const db = new Polybase({ defaultNamespace: "your-namespace" });
async function readFormResponse(
userId: string,
respId: string,
// If your private key is stored as a string, you can
// use decodeFromString to convert it to Uint8Array
userPrivateKey: Uint8Array
) {
const response = await db.collection("Response").record(respId).get();
const { data: encryptedData } = response.data;
const responseUser = await db
.collection("ResponseUser")
.record(userId + "-" + respId)
.get();
const { encryptedSymmetricKey } = responseUser.data;
const symmetricKey = await secp256k1.asymmetricDecryptFromEncoding(
userPrivateKey,
encryptedSymmetricKey,
"base64"
);
const decrypted = await aescbc.symmetricDecryptFromEncoding(
decodeFromString(symmetricKey, "base64"),
encryptedData,
"base64"
);
// Parse JSON string into form response object
return JSON.parse(decrypted);
}