(core) updates from grist-core

dependabot/npm_and_yarn/braces-3.0.3
Paul Fitzpatrick 3 months ago
commit 919cff0398

@ -8,17 +8,35 @@ on:
jobs: jobs:
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker images to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
image:
# We build two images, `grist-oss` and `grist`.
# See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images
- name: "grist-oss"
repo: "grist-core"
- name: "grist"
repo: "grist-ee"
# For now, we build it twice, with `grist-ee` being a
# backwards-compatible synoym for `grist`.
- name: "grist-ee"
repo: "grist-ee"
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Check out the ext/ directory
if: matrix.image.name != 'grist-oss'
run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: | images: |
${{ github.repository_owner }}/grist ${{ github.repository_owner }}/${{ matrix.image.name }}
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
@ -28,13 +46,16 @@ jobs:
stable stable
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub - name: Push to Docker Hub
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@ -44,3 +65,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }}

@ -10,6 +10,12 @@ on:
# Run at 5:41 UTC daily # Run at 5:41 UTC daily
- cron: '41 5 * * *' - cron: '41 5 * * *'
workflow_dispatch: workflow_dispatch:
inputs:
latest_branch:
description: "Branch from which to create the latest Docker image (default: latest_candidate)"
type: string
required: true
default_value: latest_candidate
jobs: jobs:
push_to_registry: push_to_registry:
@ -19,54 +25,98 @@ jobs:
matrix: matrix:
python-version: [3.9] python-version: [3.9]
node-version: [18.x] node-version: [18.x]
image:
# We build two images, `grist-oss` and `grist`.
# See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images
- name: "grist-oss"
repo: "grist-core"
- name: "grist"
repo: "grist-ee"
# For now, we build it twice, with `grist-ee` being a
# backwards-compatible synonym for `grist`.
- name: "grist-ee"
repo: "grist-ee"
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
ref: latest_candidate ref: ${{ inputs.latest_branch }}
- name: Check out the ext/ directory
if: matrix.image.name != 'grist-oss'
run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Prepare image but do not push it yet - name: Prepare image but do not push it yet
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
load: true load: true
tags: ${{ github.repository_owner }}/grist:latest tags: ${{ github.repository_owner }}/${{ matrix.image.name }}:experimental
cache-from: type=gha cache-from: type=gha
build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }}
- name: Use Node.js ${{ matrix.node-version }} for testing - name: Use Node.js ${{ matrix.node-version }} for testing
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Set up Python ${{ matrix.python-version }} for testing - maybe not needed - name: Set up Python ${{ matrix.python-version }} for testing - maybe not needed
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Python packages - name: Install Python packages
run: | run: |
pip install virtualenv pip install virtualenv
yarn run install:python yarn run install:python
- name: Install Node.js packages - name: Install Node.js packages
run: yarn install run: yarn install
- name: Build Node.js code - name: Build Node.js code
run: yarn run build:prod run: |
pushd ext && \
{ if [ -e package.json ] ; then yarn install --frozen-lockfile --modules-folder=../../node_modules; fi } && \
popd
yarn run build:prod
- name: Run tests - name: Run tests
run: TEST_IMAGE=${{ github.repository_owner }}/grist VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker run: TEST_IMAGE=${{ github.repository_owner }}/${{ matrix.image.name }}:experimental VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker Hub - name: Push to Docker Hub
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
push: true push: true
tags: ${{ github.repository_owner }}/grist:latest tags: ${{ github.repository_owner }}/${{ matrix.image.name }}:experimental
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-contexts: ${{ matrix.image.name != 'grist-oss' && 'ext=ext' || '' }}
update_latest_branch:
name: Update latest branch
runs-on: ubuntu-latest
needs: push_to_registry
steps:
- name: Check out the repo
uses: actions/checkout@v2
with:
ref: ${{ inputs.latest_branch }}
- name: Update latest branch - name: Update latest branch
uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1 uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1
with: with:

@ -122,6 +122,15 @@ RUN \
mv /grist/static-built/* /grist/static && \ mv /grist/static-built/* /grist/static && \
rmdir /grist/static-built rmdir /grist/static-built
# To ensure non-root users can run grist, 'other' users need read access (and execute on directories)
# This should be the case by default when copying files in.
# Only uncomment this if running into permissions issues, as it takes a long time to execute on some systems.
# RUN chmod -R o+rX /grist
# Add a user to allow de-escalating from root on startup
RUN useradd -ms /bin/bash grist
ENV GRIST_DOCKER_USER=grist \
GRIST_DOCKER_GROUP=grist
WORKDIR /grist WORKDIR /grist
# Set some default environment variables to give a setup that works out of the box when # Set some default environment variables to give a setup that works out of the box when
@ -151,5 +160,5 @@ ENV \
EXPOSE 8484 EXPOSE 8484
ENTRYPOINT ["/usr/bin/tini", "-s", "--"] ENTRYPOINT ["./sandbox/docker_entrypoint.sh"]
CMD ["./sandbox/run.sh"] CMD ["node", "./sandbox/supervisor.mjs"]

@ -83,7 +83,8 @@ If you just want a quick demo of Grist:
* Or you can see a fully in-browser build of Grist at [gristlabs.github.io/grist-static](https://gristlabs.github.io/grist-static/). * Or you can see a fully in-browser build of Grist at [gristlabs.github.io/grist-static](https://gristlabs.github.io/grist-static/).
* Or you can download Grist as a desktop app from [github.com/gristlabs/grist-desktop](https://github.com/gristlabs/grist-desktop). * Or you can download Grist as a desktop app from [github.com/gristlabs/grist-desktop](https://github.com/gristlabs/grist-desktop).
To get `grist-core` running on your computer with [Docker](https://www.docker.com/get-started), do: To get the default version of `grist-core` running on your computer
with [Docker](https://www.docker.com/get-started), do:
```sh ```sh
docker pull gristlabs/grist docker pull gristlabs/grist
@ -117,22 +118,40 @@ You can find a lot more about configuring Grist, setting up authentication,
and running it on a public server in our and running it on a public server in our
[Self-Managed Grist](https://support.getgrist.com/self-managed/) handbook. [Self-Managed Grist](https://support.getgrist.com/self-managed/) handbook.
## Activating the boot page for diagnosing problems ## Available Docker images
You can turn on a special "boot page" to inspect the status of your The default Docker image is `gristlabs/grist`. This contains all of
installation. Just visit `/boot` on your Grist server for instructions. the standard Grist functionality, as well as extra source-available
Since it is useful for the boot page to be available even when authentication code for enterprise customers taken from the the
isn't set up, you can give it a special access key by setting `GRIST_BOOT_KEY`. [grist-ee](https://github.com/gristlabs/grist-ee) repository. This
extra code is not under a free or open source license. By default,
however, the code from the `grist-ee` repository is completely inert and
inactive. This code becomes active only when an administrator enables
it by setting either `GRIST_ACTIVATION` or `GRIST_ACTIVATION_FILE`.
If you would rather use an image that contains exclusively free and
open source code, the `gristlabs/grist-oss` Docker image is available
for this purpose. It is by default functionally equivalent to the
`gristlabs/grist` image.
## The administrator panel
You can turn on a special admininistrator panel to inspect the status
of your installation. Just visit `/admin` on your Grist server for
instructions. Since it is useful for the admin panel to be
available even when authentication isn't set up, you can give it a
special access key by setting `GRIST_BOOT_KEY`.
``` ```
docker run -p 8484:8484 -e GRIST_BOOT_KEY=secret -it gristlabs/grist docker run -p 8484:8484 -e GRIST_BOOT_KEY=secret -it gristlabs/grist
``` ```
The boot page should then be available at `/boot/<GRIST_BOOT_KEY>`. We are The boot page should then be available at
starting to collect probes for common problems there. If you hit a problem that `/admin?boot-key=<GRIST_BOOT_KEY>`. We are collecting probes for
isn't covered, it would be great if you could add a probe for it in common problems there. If you hit a problem that isn't covered, it
would be great if you could add a probe for it in
[BootProbes](https://github.com/gristlabs/grist-core/blob/main/app/server/lib/BootProbes.ts). [BootProbes](https://github.com/gristlabs/grist-core/blob/main/app/server/lib/BootProbes.ts).
Or file an issue so someone else can add it, we're just getting start with this. You may instead file an issue so someone else can add it.
## Building from source ## Building from source

@ -145,6 +145,13 @@ Please log in as an administrator.`)),
description: t('Current authentication method'), description: t('Current authentication method'),
value: this._buildAuthenticationDisplay(owner), value: this._buildAuthenticationDisplay(owner),
expandedContent: this._buildAuthenticationNotice(owner), expandedContent: this._buildAuthenticationNotice(owner),
}),
dom.create(AdminSectionItem, {
id: 'session',
name: t('Session Secret'),
description: t('Key to sign sessions with'),
value: this._buildSessionSecretDisplay(owner),
expandedContent: this._buildSessionSecretNotice(owner),
}) })
]), ]),
dom.create(AdminSection, t('Version'), [ dom.create(AdminSection, t('Version'), [
@ -241,6 +248,27 @@ We recommend enabling one of these if Grist is accessible over the network or be
to multiple people.'); to multiple people.');
} }
private _buildSessionSecretDisplay(owner: IDisposableOwner) {
return dom.domComputed(
use => {
const req = this._checks.requestCheckById(use, 'session-secret');
const result = req ? use(req.result) : undefined;
if (result?.status === 'warning') {
return cssValueLabel(cssDangerText('default'));
}
return cssValueLabel(cssHappyText('configured'));
}
);
}
private _buildSessionSecretNotice(owner: IDisposableOwner) {
return t('Grist signs user session cookies with a secret key. Please set this key via the environment variable \
GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice \
in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.');
}
private _buildUpdates(owner: MultiHolder) { private _buildUpdates(owner: MultiHolder) {
// We can be in those states: // We can be in those states:
enum State { enum State {
@ -472,7 +500,11 @@ to multiple people.');
return dom.domComputed( return dom.domComputed(
use => [ use => [
...use(this._checks.probes).map(probe => { ...use(this._checks.probes).map(probe => {
const isRedundant = probe.id === 'sandboxing'; const isRedundant = [
'sandboxing',
'authentication',
'session-secret'
].includes(probe.id);
const show = isRedundant ? options.showRedundant : options.showNovel; const show = isRedundant ? options.showRedundant : options.showNovel;
if (!show) { return null; } if (!show) { return null; }
const req = this._checks.requestCheck(probe); const req = this._checks.requestCheck(probe);

@ -8,7 +8,8 @@ export type BootProbeIds =
'sandboxing' | 'sandboxing' |
'system-user' | 'system-user' |
'authentication' | 'authentication' |
'websockets' 'websockets' |
'session-secret'
; ;
export interface BootProbeResult { export interface BootProbeResult {

@ -29,6 +29,9 @@ export class User extends BaseEntity {
@Column({name: 'first_login_at', type: Date, nullable: true}) @Column({name: 'first_login_at', type: Date, nullable: true})
public firstLoginAt: Date | null; public firstLoginAt: Date | null;
@Column({name: 'last_connection_at', type: Date, nullable: true})
public lastConnectionAt: Date | null;
@OneToOne(type => Organization, organization => organization.owner) @OneToOne(type => Organization, organization => organization.owner)
public personalOrg: Organization; public personalOrg: Organization;

@ -395,14 +395,6 @@ export class UsersManager {
user.name = (profile && (profile.name || email.split('@')[0])) || ''; user.name = (profile && (profile.name || email.split('@')[0])) || '';
needUpdate = true; needUpdate = true;
} }
if (profile && !user.firstLoginAt) {
// set first login time to now (remove milliseconds for compatibility with other
// timestamps in db set by typeorm, and since second level precision is fine)
const nowish = new Date();
nowish.setMilliseconds(0);
user.firstLoginAt = nowish;
needUpdate = true;
}
if (!user.picture && profile && profile.picture) { if (!user.picture && profile && profile.picture) {
// Set the user's profile picture if our provider knows it. // Set the user's profile picture if our provider knows it.
user.picture = profile.picture; user.picture = profile.picture;
@ -432,6 +424,25 @@ export class UsersManager {
user.options = {...(user.options ?? {}), authSubject: userOptions.authSubject}; user.options = {...(user.options ?? {}), authSubject: userOptions.authSubject};
needUpdate = true; needUpdate = true;
} }
// get date of now (remove milliseconds for compatibility with other
// timestamps in db set by typeorm, and since second level precision is fine)
const nowish = new Date();
nowish.setMilliseconds(0);
if (profile && !user.firstLoginAt) {
// set first login time to now
user.firstLoginAt = nowish;
needUpdate = true;
}
const getTimestampStartOfDay = (date: Date) => {
const timestamp = Math.floor(date.getTime() / 1000); // unix timestamp seconds from epoc
const startOfDay = timestamp - (timestamp % 86400 /*24h*/); // start of a day in seconds since epoc
return startOfDay;
};
if (!user.lastConnectionAt || getTimestampStartOfDay(user.lastConnectionAt) !== getTimestampStartOfDay(nowish)) {
user.lastConnectionAt = nowish;
needUpdate = true;
}
if (needUpdate) { if (needUpdate) {
login.user = user; login.user = user;
await manager.save([user, login]); await manager.save([user, login]);

@ -1,5 +1,5 @@
import {User} from 'app/gen-server/entity/User';
import {makeId} from 'app/server/lib/idUtils'; import {makeId} from 'app/server/lib/idUtils';
import {chunk} from 'lodash';
import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; import {MigrationInterface, QueryRunner, TableColumn} from "typeorm";
export class UserUUID1663851423064 implements MigrationInterface { export class UserUUID1663851423064 implements MigrationInterface {
@ -16,11 +16,20 @@ export class UserUUID1663851423064 implements MigrationInterface {
// Updating so many rows in a multiple queries is not ideal. We will send updates in chunks. // Updating so many rows in a multiple queries is not ideal. We will send updates in chunks.
// 300 seems to be a good number, for 24k rows we have 80 queries. // 300 seems to be a good number, for 24k rows we have 80 queries.
const userList = await queryRunner.manager.createQueryBuilder() const userList = await queryRunner.manager.createQueryBuilder()
.select("users") .select(["users.id", "users.ref"])
.from(User, "users") .from("users", "users")
.getMany(); .getMany();
userList.forEach(u => u.ref = makeId()); userList.forEach(u => u.ref = makeId());
await queryRunner.manager.save(userList, { chunk: 300 });
const userChunks = chunk(userList, 300);
for (const users of userChunks) {
await queryRunner.connection.transaction(async manager => {
const queries = users.map((user: any, _index: number, _array: any[]) => {
return queryRunner.manager.update("users", user.id, user);
});
await Promise.all(queries);
});
}
// We are not making this column unique yet, because it can fail // We are not making this column unique yet, because it can fail
// if there are some old workers still running, and any new user // if there are some old workers still running, and any new user

@ -1,5 +1,5 @@
import {User} from 'app/gen-server/entity/User';
import {makeId} from 'app/server/lib/idUtils'; import {makeId} from 'app/server/lib/idUtils';
import {chunk} from 'lodash';
import {MigrationInterface, QueryRunner} from "typeorm"; import {MigrationInterface, QueryRunner} from "typeorm";
export class UserRefUnique1664528376930 implements MigrationInterface { export class UserRefUnique1664528376930 implements MigrationInterface {
@ -9,12 +9,21 @@ export class UserRefUnique1664528376930 implements MigrationInterface {
// Update users that don't have unique ref set. // Update users that don't have unique ref set.
const userList = await queryRunner.manager.createQueryBuilder() const userList = await queryRunner.manager.createQueryBuilder()
.select("users") .select(["users.id", "users.ref"])
.from(User, "users") .from("users", "users")
.where("ref is null") .where("users.ref is null")
.getMany(); .getMany();
userList.forEach(u => u.ref = makeId()); userList.forEach(u => u.ref = makeId());
await queryRunner.manager.save(userList, {chunk: 300});
const userChunks = chunk(userList, 300);
for (const users of userChunks) {
await queryRunner.connection.transaction(async manager => {
const queries = users.map((user: any, _index: number, _array: any[]) => {
return queryRunner.manager.update("users", user.id, user);
});
await Promise.all(queries);
});
}
// Mark column as unique and non-nullable. // Mark column as unique and non-nullable.
const users = (await queryRunner.getTable('users'))!; const users = (await queryRunner.getTable('users'))!;

@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner, TableColumn} from 'typeorm';
export class UserLastConnection1713186031023 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
const sqlite = queryRunner.connection.driver.options.type === 'sqlite';
const datetime = sqlite ? "datetime" : "timestamp with time zone";
await queryRunner.addColumn('users', new TableColumn({
name: 'last_connection_at',
type: datetime,
isNullable: true
}));
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropColumn('users', 'last_connection_at');
}
}

@ -6,6 +6,7 @@ import { GristServer } from 'app/server/lib/GristServer';
import * as express from 'express'; import * as express from 'express';
import WS from 'ws'; import WS from 'ws';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { DEFAULT_SESSION_SECRET } from 'app/server/lib/coreCreator';
/** /**
* Self-diagnostics useful when installing Grist. * Self-diagnostics useful when installing Grist.
@ -61,6 +62,7 @@ export class BootProbes {
this._probes.push(_sandboxingProbe); this._probes.push(_sandboxingProbe);
this._probes.push(_authenticationProbe); this._probes.push(_authenticationProbe);
this._probes.push(_webSocketsProbe); this._probes.push(_webSocketsProbe);
this._probes.push(_sessionSecretProbe);
this._probeById = new Map(this._probes.map(p => [p.id, p])); this._probeById = new Map(this._probes.map(p => [p.id, p]));
} }
} }
@ -284,3 +286,17 @@ const _authenticationProbe: Probe = {
}; };
}, },
}; };
const _sessionSecretProbe: Probe = {
id: 'session-secret',
name: 'Session secret',
apply: async(server, req) => {
const usingDefaultSessionSecret = server.create.sessionSecret() === DEFAULT_SESSION_SECRET;
return {
status: usingDefaultSessionSecret ? 'warning' : 'success',
details: {
"GRIST_SESSION_SECRET": process.env.GRIST_SESSION_SECRET ? "set" : "not set",
}
};
},
};

@ -1883,6 +1883,22 @@ export class FlexServer implements GristServer {
const probes = new BootProbes(this.app, this, '/api', adminMiddleware); const probes = new BootProbes(this.app, this, '/api', adminMiddleware);
probes.addEndpoints(); probes.addEndpoints();
this.app.post('/api/admin/restart', requireInstallAdmin, expressWrap(async (req, resp) => {
const newConfig = req.body.newConfig;
resp.on('finish', () => {
// If we have IPC with parent process (e.g. when running under
// Docker) tell the parent that we have a new environment so it
// can restart us.
if (process.send) {
process.send({ action: 'restart', newConfig });
}
});
// On the topic of http response codes, thus spake MDN:
// "409: This response is sent when a request conflicts with the current state of the server."
const status = process.send ? 200 : 409;
return resp.status(status).send();
}));
// Restrict this endpoint to install admins // Restrict this endpoint to install admins
this.app.get('/api/install/prefs', requireInstallAdmin, expressWrap(async (_req, resp) => { this.app.get('/api/install/prefs', requireInstallAdmin, expressWrap(async (_req, resp) => {
const activation = await this._activations.current(); const activation = await this._activations.current();

@ -3,11 +3,14 @@ import { checkMinIOBucket, checkMinIOExternalStorage,
import { makeSimpleCreator } from 'app/server/lib/ICreate'; import { makeSimpleCreator } from 'app/server/lib/ICreate';
import { Telemetry } from 'app/server/lib/Telemetry'; import { Telemetry } from 'app/server/lib/Telemetry';
export const DEFAULT_SESSION_SECRET =
'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh';
export const makeCoreCreator = () => makeSimpleCreator({ export const makeCoreCreator = () => makeSimpleCreator({
deploymentType: 'core', deploymentType: 'core',
// This can and should be overridden by GRIST_SESSION_SECRET // This can and should be overridden by GRIST_SESSION_SECRET
// (or generated randomly per install, like grist-omnibus does). // (or generated randomly per install, like grist-omnibus does).
sessionSecret: 'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh', sessionSecret: DEFAULT_SESSION_SECRET,
storage: [ storage: [
{ {
name: 'minio', name: 'minio',

@ -6,10 +6,10 @@ import {GristServer} from 'app/server/lib/GristServer';
import {fromCallback} from 'app/server/lib/serverUtils'; import {fromCallback} from 'app/server/lib/serverUtils';
import {Sessions} from 'app/server/lib/Sessions'; import {Sessions} from 'app/server/lib/Sessions';
import {promisifyAll} from 'bluebird'; import {promisifyAll} from 'bluebird';
import * as crypto from 'crypto';
import * as express from 'express'; import * as express from 'express';
import assignIn = require('lodash/assignIn'); import assignIn = require('lodash/assignIn');
import * as path from 'path'; import * as path from 'path';
import * as shortUUID from "short-uuid";
export const cookieName = process.env.GRIST_SESSION_COOKIE || 'grist_sid'; export const cookieName = process.env.GRIST_SESSION_COOKIE || 'grist_sid';
@ -118,7 +118,10 @@ export function initGristSessions(instanceRoot: string, server: GristServer) {
// cookie could be stolen (with some effort) by the custom domain's owner, we limit the damage // cookie could be stolen (with some effort) by the custom domain's owner, we limit the damage
// by only honoring custom-domain cookies for requests to that domain. // by only honoring custom-domain cookies for requests to that domain.
const generateId = (req: RequestWithOrg) => { const generateId = (req: RequestWithOrg) => {
const uid = shortUUID.generate(); // Generate 256 bits of cryptographically random data to use as the session ID.
// This ensures security against brute-force session hijacking even without signing the session ID.
const randomNumbers = crypto.getRandomValues(new Uint8Array(32));
const uid = Buffer.from(randomNumbers).toString("hex");
return req.isCustomHost ? `c-${uid}@${req.org}@${req.get('host')}` : `g-${uid}`; return req.isCustomHost ? `c-${uid}@${req.org}@${req.get('host')}` : `g-${uid}`;
}; };
const sessionSecret = server.create.sessionSecret(); const sessionSecret = server.create.sessionSecret();

@ -21,8 +21,8 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ?
// Database fields that we permit in entities but don't want to cross the api. // Database fields that we permit in entities but don't want to cross the api.
const INTERNAL_FIELDS = new Set([ const INTERNAL_FIELDS = new Set([
'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId', 'apiKey', 'billingAccountId', 'firstLoginAt', 'lastConnectionAt', 'filteredOut', 'ownerId', 'gracePeriodStart',
'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin', 'stripeCustomerId', 'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
'authSubject', 'usage', 'createdBy' 'authSubject', 'usage', 'createdBy'
]); ]);

@ -5,7 +5,11 @@ set -e
PROJECT="" PROJECT=""
if [[ -e ext/app ]]; then if [[ -e ext/app ]]; then
PROJECT="tsconfig-ext.json" PROJECT="tsconfig-ext.json"
echo "Using extra app directory"
else
echo "No extra app directory found"
fi fi
WEBPACK_CONFIG=buildtools/webpack.config.js WEBPACK_CONFIG=buildtools/webpack.config.js
if [[ -e ext/buildtools/webpack.config.js ]]; then if [[ -e ext/buildtools/webpack.config.js ]]; then
# Allow webpack config file to be replaced (useful # Allow webpack config file to be replaced (useful

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# This checks out the ext/ directory from the extra repo (e.g.
# grist-ee or grist-desktop) depending on the supplied repo name.
set -e
repo=$1
dir=$(dirname $0)
ref=$(cat $dir/.$repo-version)
git clone --branch $ref --depth 1 --filter=tree:0 "https://github.com/gristlabs/$repo"
pushd $repo
git sparse-checkout set ext
git checkout
popd
mv $repo/ext .
rm -rf $repo

@ -0,0 +1,297 @@
# Database
> [!WARNING]
> This documentation is meant to describe the state of the database. The reader should be aware that some undocumented changes may have been done after its last updates, and for this purpose should check the git history of this file.
>
> Also contributions are welcome! :heart:
Grist manages two databases:
1. The Home Database;
2. The Document Database (also known as "the grist document");
The Home database is responsible for things related to the instance, such as:
- the users and the groups registered on the instance,
- the billing,
- the organisations (also called sites), the workspaces,
- the documents' metadata (such as ID, name, or workspace under which it is located);
- the access permissions (ACLs) to organisations, workspaces and documents (access to the content of the document is controlled by the document itself);
A Grist Document contains data such as:
- The tables, pages, views data;
- The ACL *inside* to access to all or part of tables (rows or columns);
## The Document Database
### Inspecting the Document
A Grist Document (with the `.grist` extension) is actually a SQLite database. You may download a document like [this one](https://api.getgrist.com/o/templates/api/docs/keLK5sVeyfPkxyaXqijz2x/download?template=false&nohistory=false) and inspect its content using a tool such as the `sqlite3` command:
````
$ sqlite3 Flashcards.grist
sqlite> .tables
Flashcards_Data _grist_TabBar
Flashcards_Data_summary_Card_Set _grist_TabItems
GristDocTour _grist_TableViews
_grist_ACLMemberships _grist_Tables
_grist_ACLPrincipals _grist_Tables_column
_grist_ACLResources _grist_Triggers
_grist_ACLRules _grist_Validations
_grist_Attachments _grist_Views
_grist_Cells _grist_Views_section
_grist_DocInfo _grist_Views_section_field
_grist_External_database _gristsys_Action
_grist_External_table _gristsys_ActionHistory
_grist_Filters _gristsys_ActionHistoryBranch
_grist_Imports _gristsys_Action_step
_grist_Pages _gristsys_FileInfo
_grist_REPL_Hist _gristsys_Files
_grist_Shares _gristsys_PluginData
````
:warning: If you want to ensure that you will not alter a document's contents, make a backup copy beforehand.
### The migrations
The migrations are handled in the Python sandbox in this code:
https://github.com/gristlabs/grist-core/blob/main/sandbox/grist/migrations.py
For more information, please consult [the documentation for migrations](./migrations.md).
## The Home Database
The home database may either be a SQLite or a PostgreSQL database depending on how the Grist instance has been installed. For details, please refer to the `TYPEORM_*` env variables in the [README](https://github.com/gristlabs/grist-core/blob/main/README.md#database-variables).
Unless otherwise configured, the home database is a SQLite file. In the default Docker image, it is stored at this location: `/persist/home.sqlite3`.
The schema below is the same (except for minor differences in the column types), regardless of what the database type is.
### The Schema
The database schema is the following:
![Schema of the home database](./images/homedb-schema.svg)
> [!NOTE]
> For simplicity's sake, we have removed tables related to the billing and to the migrations.
If you want to generate the above schema by yourself, you may run the following command using [SchemaCrawler](https://www.schemacrawler.com/) ([a docker image is available for a quick run](https://www.schemacrawler.com/docker-image.html)):
````bash
# You may adapt the --database argument to fit with the actual file name
# You may also remove the `--grep-tables` option and all that follows to get the full schema.
$ schemacrawler --server=sqlite --database=landing.db --info-level=standard \
--portable-names --command=schema --output-format=svg \
--output-file=/tmp/graph.svg \
--grep-tables="products|billing_accounts|limits|billing_account_managers|activations|migrations" \
--invert-match
````
### `orgs` table
Stores organisations (also called "Team sites") information.
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| name | The name as displayed in the UI |
| domain | The part that should be added in the URL |
| owner | The id of the user who owns the org |
| host | ??? |
### `workspaces` table
Stores workspaces information.
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| name | The name as displayed in the UI |
| org_id | The organisation to which the workspace belongs |
| removed_at | If not null, stores the date when the workspaces has been placed in the trash (it will be hard deleted after 30 days) |
### `docs` table
Stores document information that is not portable, which means that it does not store the document data nor the ACL rules (see the "Document Database" section).
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| name | The name as displayed in the UI |
| workspace_id | The workspace the document belongs to |
| is_pinned | Whether the document has been pinned or not |
| url_id | Short version of the `id`, as displayed in the URL |
| removed_at | If not null, stores the date when the workspaces has been placed in the trash (it will be hard deleted after 30 days) |
| options | Serialized options as described in the [DocumentOptions](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/UserAPI.ts#L125-L135) interface |
| grace_period_start | Specific to getgrist.com (TODO describe it) |
| usage | stats about the document (see [DocumentUsage](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/DocUsage.ts)) |
| trunk_id | If set, the current document is a fork (only from a tutorial), and this column references the original document |
| type | If set, the current document is a special one (as specified in [DocumentType](https://github.com/gristlabs/grist-core/blob/4567fad94787c20f65db68e744c47d5f44b932e4/app/common/UserAPI.ts#L123)) |
### `aliases` table
Aliases for documents.
FIXME: What's the difference between `docs.url_id` and `alias.url_id`?
| Column name | Description |
| ------------- | -------------- |
| url_id | The URL alias for the doc_id |
| org_id | The organisation the document belongs to |
| doc_id | The document id |
### `acl_rules` table
Permissions to access either a document, workspace or an organisation.
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| permissions | The permissions granted to the group. See below. |
| type | Either equals to `ACLRuleOrg`, `ACLRuleWs` or `ACLRuleDoc` |
| org_id | The org id associated to this ACL (if set, workspace_id and doc_id are null) |
| workspace_id | The workspace id associated to this ACL (if set, doc_id and org_id are null) |
| doc_id | The document id associated to this ACL (if set, workspace_id and org_id are null) |
| group_id | The group of users for which the ACL applies |
<a name="acl-permissions"></a>
The permissions are stored as an integer which is read in its binary form which allows to make bitwise operations:
| Name | Value (binary) | Description |
| --------------- | --------------- | --------------- |
| VIEW | +0b00000001 | can view |
| UPDATE | +0b00000010 | can update |
| ADD | +0b00000100 | can add |
| REMOVE | +0b00001000 | can remove |
| SCHEMA_EDIT | +0b00010000 | can change schema of tables |
| ACL_EDIT | +0b00100000 | can edit the ACL (docs) or manage the teams (orgs and workspaces) of the resource |
| (reserved) | +0b01000000 | (reserved bit for the future) |
| PUBLIC | +0b10000000 | virtual bit meaning that the resource is shared publicly (not currently used) |
You notice that the permissions can be then composed:
- EDITOR permissions = `VIEW | UPDATE | ADD | REMOVE` = `0b00000001+0b00000010+0b00000100+0b00001000` = `0b00001111` = `15`
- ADMIN permissions = `EDITOR | SCHEMA_EDIT` = `0b00001111+0b00010000` = `0b00011111` = `31`
- OWNER permissions = `ADMIN | ACL_EDIT` = `0b00011111+0b00100000` = `0b0011111` = `63`
For more details about that part, please refer [to the code](https://github.com/gristlabs/grist-core/blob/192e2f36ba77ec67069c58035d35205978b9215e/app/gen-server/lib/Permissions.ts).
### `secrets` table
Stores secret informations related to documents, so the document may not store them (otherwise someone who downloads a doc may access them). Used to store the unsubscribe key and the target url of Webhooks.
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| value | The value of the secret (despite the table name, its stored unencrypted) |
| doc_id | The document id |
### `prefs` table
Stores special grants for documents for anyone having the key.
| Column name | Description |
| ------------- | -------------- |
| id | The primary key |
| key | A long string secret to identify the share. Suitable for URLs. Unique across the database / installation. |
| link_id | A string to identify the share. This identifier is common to the home database and the document specified by docId. It need only be unique within that document, and is not a secret. | doc_id | The document to which the share belongs |
| options | Any overall qualifiers on the share |
For more information, please refer [to the comments in the code](https://github.com/gristlabs/grist-core/blob/192e2f36ba77ec67069c58035d35205978b9215e/app/gen-server/entity/Share.ts).
### `groups` table
The groups are entities that may contain either other groups and/or users.
| Column name | Description |
|--------------- | --------------- |
| id | The primary key |
| name | The name (see the 5 types of groups below) |
Only 5 types of groups exist, which corresponds actually to Roles (for the permissions, please refer to the [ACL rules permissions details](#acl-permissions)):
- `owners` (see the `OWNERS` permissions)
- `editors` (see the `EDITORS` permissions)
- `viewers` (see the `VIEWS` permissions)
- `members`
- `guests`
`viewers`, `members` and `guests` have basically the same rights (like viewers), the only difference between them is that:
- `viewers` are explicitly allowed to view the resource and its descendants;
- `members` are specific to the organisations and are meant to allow access to be granted to individual documents or workspaces, rather than the full team site.
- `guests` are (FIXME: help please on this one :))
Each time a resource is created, the groups corresponding to the roles above are created (except the `members` which are specific to organisations).
### `group_groups` table
The table which allows groups to contain other groups. It is also used for the inheritance mechanism (see below).
| Column name | Description |
|--------------- | --------------- |
| group_id | The id of the group containing the subgroup |
| subgroup_id | The id of the subgroup |
### `group_users` table
The table which assigns users to groups.
| Column name | Description |
|--------------- | --------------- |
| group_id | The id of the group containing the user |
| user_id | The id of the user |
### `groups`, `group_groups`, `group_users` and inheritances
We mentioned earlier that the groups currently holds the roles with the associated permissions.
The database stores the inheritances of rights as described below.
Let's imagine that a user is granted the role of *Owner* for the "Org1" organisation, s/he therefore belongs to the group "Org1 Owners" (whose ID is `id_org1_owner_grp`) which also belongs to the "WS1 Owners" (whose ID is `id_ws1_owner_grp`) by default. In other words, this user is by default owner of both the Org1 organization and of the WS1 workspace.
The below schema illustrates both the inheritance of between the groups and the state of the database:
![BDD state by default](./images/BDD-doc-inheritance-default.svg) <!-- Use diagrams.net and import ./images/BDD.drawio to edit this image -->
This inheritance can be changed through the Users management popup in the Contextual Menu for the Workspaces:
![The drop-down list after "Inherit access:" in the workspaces Users Management popup](./images/ws-users-management-popup.png)
If you change the inherit access to "View Only", here is what happens:
![BDD state after inherit access has changed, the `group_groups.group_id` value has changed](./images/BDD-doc-inheritance-after-change.svg) <!-- Use diagrams.net and import ./images/BDD.drawio to edit this image -->
The Org1 owners now belongs to the "WS1 Viewers" group, and the user despite being *Owner* of "Org1" can only view the workspace WS1 and its documents because s/he only gets the Viewer role for this workspace. Regarding the database, `group_groups` which holds the group inheritance has been updated, so the parent group for `id_org1_owner_grp` is now `id_ws1_viewers_grp`.
### `users` table
Stores `users` information.
| Column name | Description |
|--------------- | --------------- |
| id | The primary key |
| name | The user's name |
| api_key | If generated, the [HTTP API Key](https://support.getgrist.com/rest-api/) used to authenticate the user |
| picture | The URL to the user's picture (must be provided by the SSO Identity Provider) |
| first_login_at | The date of the first login |
| is_first_time_user | Whether the user discovers Grist (used to trigger the Welcome Tour) |
| options | Serialized options as described in [UserOptions](https://github.com/gristlabs/grist-core/blob/513e13e6ab57c918c0e396b1d56686e45644ee1a/app/common/UserAPI.ts#L169-L179) interface |
| connect_id | Used by [GristConnect](https://github.com/gristlabs/grist-ee/blob/5ae19a7dfb436c8a3d67470b993076e51cf83f21/ext/app/server/lib/GristConnect.ts) in Enterprise Edition to identify user in external provider |
| ref | Used to identify a user in the automated tests |
### `logins` table
Stores information related to the identification.
> [!NOTE]
> A user may have many `logins` records associated to him/her, like several emails used for identification.
| Column name | Description |
|--------------- | --------------- |
| id | The primary key |
| user_id | The user's id |
| email | The normalized email address used for equality and indexing (specifically converted to lower case) |
| display_email | The user's email address as displayed in the UI |
### The migrations
The database migrations are handled by TypeORM ([documentation](https://typeorm.io/migrations)). The migration files are located at `app/gen-server/migration` and are run at startup (so you don't have to worry about running them yourself).

@ -130,6 +130,7 @@ Check out this repository: https://github.com/gristlabs/grist-widget#readme
Some documentation to help you starting developing: Some documentation to help you starting developing:
- [Overview of Grist Components](./overview.md) - [Overview of Grist Components](./overview.md)
- [The database](./database.md)
- [GrainJS & Grist Front-End Libraries](./grainjs.md) - [GrainJS & Grist Front-End Libraries](./grainjs.md)
- [GrainJS Documentation](https://github.com/gristlabs/grainjs/) (The library used to build the DOM) - [GrainJS Documentation](https://github.com/gristlabs/grainjs/) (The library used to build the DOM)
- [The user support documentation](https://support.getgrist.com/) - [The user support documentation](https://support.getgrist.com/)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 238 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 191 KiB

@ -0,0 +1,234 @@
<mxfile host="app.diagrams.net" modified="2024-05-03T07:57:06.222Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0" etag="d5mpqxfjE_YjavJEgyO5" version="24.3.1" type="device" pages="2">
<diagram name="doc - Inheritance : default" id="HMcLKXGEOIWPtuluTPJ-">
<mxGraphModel dx="1654" dy="872" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="uSf0n1dOcknmwi0iKCrK-0" />
<mxCell id="uSf0n1dOcknmwi0iKCrK-1" parent="uSf0n1dOcknmwi0iKCrK-0" />
<mxCell id="uSf0n1dOcknmwi0iKCrK-2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="uSf0n1dOcknmwi0iKCrK-1" source="uSf0n1dOcknmwi0iKCrK-3" target="uSf0n1dOcknmwi0iKCrK-4" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-3" value="Org1" style="rounded=0;whiteSpace=wrap;html=1;" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1">
<mxGeometry x="109" y="493.5" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-4" value="Workspace1" style="rounded=0;whiteSpace=wrap;html=1;" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1">
<mxGeometry x="109" y="317" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="uSf0n1dOcknmwi0iKCrK-1" source="uSf0n1dOcknmwi0iKCrK-6" target="uSf0n1dOcknmwi0iKCrK-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" parent="uSf0n1dOcknmwi0iKCrK-1" source="uSf0n1dOcknmwi0iKCrK-22" target="uSf0n1dOcknmwi0iKCrK-6" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="459" y="595" />
<mxPoint x="548" y="595" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-21" value="" style="group;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1" connectable="0">
<mxGeometry x="437.5" y="611" width="43" height="65" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-22" value="Some user" style="html=1;verticalLabelPosition=bottom;align=center;labelBackgroundColor=#ffffff;verticalAlign=top;strokeWidth=2;strokeColor=#0080F0;shadow=0;dashed=0;shape=mxgraph.ios7.icons.user;" parent="uSf0n1dOcknmwi0iKCrK-21" vertex="1">
<mxGeometry x="6.5" y="35" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-24" value="group_users" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;fontStyle=1" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1">
<mxGeometry x="713" y="593" width="359" height="118" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-25" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-24" vertex="1">
<mxGeometry y="30" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-26" value="&lt;b&gt;group_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-25" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-27" value="&lt;b&gt;user_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-25" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-28" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-24" vertex="1">
<mxGeometry y="74" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-29" value="id_org1_owner_grp" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-28" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-30" value="id_some_user" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-28" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-31" value="group_groups" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;fontStyle=1" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1">
<mxGeometry x="711" y="288" width="359" height="118" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-32" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-31" vertex="1">
<mxGeometry y="30" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-33" value="&lt;b&gt;group_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-32" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-34" value="&lt;b&gt;subgroup_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-32" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-35" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-31" vertex="1">
<mxGeometry y="74" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-36" value="&lt;div&gt;id_ws1_owner_grp&lt;/div&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-35" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-37" value="id_org1_owner_grp" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="uSf0n1dOcknmwi0iKCrK-35" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-5" value="" style="group;labelBackgroundColor=default;labelBorderColor=none;" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1" connectable="0">
<mxGeometry x="523" y="464" width="50" height="78" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-6" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;Org1 Owners&lt;/font&gt;" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#505050;shape=mxgraph.mscae.intune.user_group;labelBackgroundColor=default;spacingTop=9;" parent="uSf0n1dOcknmwi0iKCrK-5" vertex="1">
<mxGeometry y="41" width="50" height="37" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-7" value="" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://cdn0.iconfinder.com/data/icons/phosphor-fill-vol-2/256/crown-simple-fill-128.png" parent="uSf0n1dOcknmwi0iKCrK-5" vertex="1">
<mxGeometry x="3.5" width="43" height="43" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-9" value="" style="group" parent="uSf0n1dOcknmwi0iKCrK-1" vertex="1" connectable="0">
<mxGeometry x="523" y="281" width="50" height="83" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-10" value="&lt;div style=&quot;font-size: 16px; padding-left: 0px; margin-top: 10px;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;Ws1 Owners&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#505050;shape=mxgraph.mscae.intune.user_group" parent="uSf0n1dOcknmwi0iKCrK-9" vertex="1">
<mxGeometry y="46" width="50" height="37" as="geometry" />
</mxCell>
<mxCell id="uSf0n1dOcknmwi0iKCrK-11" value="" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://cdn0.iconfinder.com/data/icons/phosphor-fill-vol-2/256/crown-simple-fill-128.png" parent="uSf0n1dOcknmwi0iKCrK-9" vertex="1">
<mxGeometry x="3.5" width="43" height="43" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram name="doc - inheritance : after change" id="ejp4Dg6iXyrIoHg3_VKk">
<mxGraphModel dx="1654" dy="872" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="cy84TbzhjBedF44X58Xk-0" />
<mxCell id="cy84TbzhjBedF44X58Xk-1" parent="cy84TbzhjBedF44X58Xk-0" />
<mxCell id="cy84TbzhjBedF44X58Xk-2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="cy84TbzhjBedF44X58Xk-1" source="cy84TbzhjBedF44X58Xk-3" target="cy84TbzhjBedF44X58Xk-4" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-3" value="Org1" style="rounded=0;whiteSpace=wrap;html=1;" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="109" y="493.5" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-4" value="Workspace1" style="rounded=0;whiteSpace=wrap;html=1;" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="109" y="317" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;" parent="cy84TbzhjBedF44X58Xk-1" source="cy84TbzhjBedF44X58Xk-24" target="cy84TbzhjBedF44X58Xk-27" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" parent="cy84TbzhjBedF44X58Xk-1" source="cy84TbzhjBedF44X58Xk-8" target="cy84TbzhjBedF44X58Xk-24" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="459" y="595" />
<mxPoint x="548" y="595" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-7" value="" style="group" parent="cy84TbzhjBedF44X58Xk-1" vertex="1" connectable="0">
<mxGeometry x="437.5" y="611" width="43" height="65" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-8" value="Some user" style="html=1;verticalLabelPosition=bottom;align=center;labelBackgroundColor=#ffffff;verticalAlign=top;strokeWidth=2;strokeColor=#0080F0;shadow=0;dashed=0;shape=mxgraph.ios7.icons.user;" parent="cy84TbzhjBedF44X58Xk-7" vertex="1">
<mxGeometry x="6.5" y="35" width="30" height="30" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-9" value="group_users" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;fontStyle=1" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="758" y="589" width="359" height="118" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-10" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-9" vertex="1">
<mxGeometry y="30" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-11" value="&lt;b&gt;group_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-10" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-12" value="&lt;b&gt;user_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-10" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-13" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-9" vertex="1">
<mxGeometry y="74" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-14" value="id_org1_owner_grp" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-13" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-15" value="id_some_user" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="cy84TbzhjBedF44X58Xk-13" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-23" value="" style="group;labelBackgroundColor=default;labelBorderColor=none;" parent="cy84TbzhjBedF44X58Xk-1" vertex="1" connectable="0">
<mxGeometry x="523" y="464" width="50" height="78" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-24" value="&lt;font style=&quot;font-size: 16px;&quot;&gt;Org1 Owners&lt;/font&gt;" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#505050;shape=mxgraph.mscae.intune.user_group;labelBackgroundColor=default;spacingTop=9;" parent="cy84TbzhjBedF44X58Xk-23" vertex="1">
<mxGeometry y="41" width="50" height="37" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-25" value="" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://cdn0.iconfinder.com/data/icons/phosphor-fill-vol-2/256/crown-simple-fill-128.png" parent="cy84TbzhjBedF44X58Xk-23" vertex="1">
<mxGeometry x="3.5" width="43" height="43" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-26" value="" style="group" parent="cy84TbzhjBedF44X58Xk-1" vertex="1" connectable="0">
<mxGeometry x="401" y="281" width="50" height="83" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-27" value="&lt;div style=&quot;font-size: 16px; padding-left: 0px; margin-top: 10px;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;Ws1 Owners&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#505050;shape=mxgraph.mscae.intune.user_group" parent="cy84TbzhjBedF44X58Xk-26" vertex="1">
<mxGeometry y="46" width="50" height="37" as="geometry" />
</mxCell>
<mxCell id="cy84TbzhjBedF44X58Xk-28" value="" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://cdn0.iconfinder.com/data/icons/phosphor-fill-vol-2/256/crown-simple-fill-128.png" parent="cy84TbzhjBedF44X58Xk-26" vertex="1">
<mxGeometry x="3.5" width="43" height="43" as="geometry" />
</mxCell>
<mxCell id="Cej_1C5x5ezJ23L6Zn-K-0" value="" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://cdn4.iconfinder.com/data/icons/essentials-72/24/039_-_Cross-128.png" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="406" y="434.5" width="41" height="41" as="geometry" />
</mxCell>
<mxCell id="Cej_1C5x5ezJ23L6Zn-K-2" value="NEW" style="dashed=0;html=1;rounded=1;strokeColor=#6554C0;fontSize=12;align=center;fontStyle=1;strokeWidth=2;fontColor=#6554C0" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="668" y="445" width="50" height="20" as="geometry" />
</mxCell>
<mxCell id="Cej_1C5x5ezJ23L6Zn-K-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="cy84TbzhjBedF44X58Xk-1" source="cy84TbzhjBedF44X58Xk-24" target="Cej_1C5x5ezJ23L6Zn-K-5" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Cej_1C5x5ezJ23L6Zn-K-5" value="&lt;div style=&quot;font-size: 16px; padding-left: 0px; margin-top: 10px;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;Ws1 Viewers&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;font-size: 16px; padding-left: 0px; margin-top: 10px;&quot;&gt;&lt;font style=&quot;font-size: 16px;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255);&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#505050;shape=mxgraph.mscae.intune.user_group" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="634" y="327" width="50" height="37" as="geometry" />
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-0" value="group_groups" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;fontStyle=1" parent="cy84TbzhjBedF44X58Xk-1" vertex="1">
<mxGeometry x="758" y="288" width="359" height="118" as="geometry" />
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-1" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-0" vertex="1">
<mxGeometry y="30" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-2" value="&lt;b&gt;group_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-1" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-3" value="&lt;b&gt;subgroup_id&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-1" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-4" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-0" vertex="1">
<mxGeometry y="74" width="359" height="44" as="geometry" />
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-5" value="&lt;div&gt;&lt;strike&gt;&lt;b&gt;&lt;font color=&quot;#ff3333&quot;&gt;id_ws1_owner_grp&lt;/font&gt;&lt;/b&gt;&lt;/strike&gt;&lt;/div&gt;&lt;b&gt;&lt;font color=&quot;#00cc00&quot;&gt;id_ws1_viewers_grp&lt;/font&gt;&lt;/b&gt;" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-4" vertex="1">
<mxGeometry width="179" height="44" as="geometry">
<mxRectangle width="179" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="rRc6SIQjJta77vA1fWc8-6" value="id_org1_owner_grp" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" parent="rRc6SIQjJta77vA1fWc8-4" vertex="1">
<mxGeometry x="179" width="180" height="44" as="geometry">
<mxRectangle width="180" height="44" as="alternateBounds" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

@ -0,0 +1,609 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (20230911.1827)
-->
<!-- Title: SchemaCrawler_Diagram Pages: 1 -->
<svg width="1587pt" height="914pt"
viewBox="0.00 0.00 1587.00 914.26" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 910.26)">
<title>SchemaCrawler_Diagram</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-910.26 1583,-910.26 1583,4 -4,4"/>
<text text-anchor="start" x="1304" y="-31.7" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text>
<text text-anchor="start" x="1400.88" y="-31.7" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 16.21.2</text>
<text text-anchor="start" x="1304" y="-10.7" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text>
<text text-anchor="start" x="1401" y="-10.7" font-family="Helvetica,sans-Serif" font-size="14.00">2024&#45;04&#45;15 14:21:22</text>
<polygon fill="none" stroke="#888888" points="1301,-4 1301,-48 1571,-48 1571,-4 1301,-4"/>
<!-- acl_rules_53bd8961 -->
<g id="node1" class="node">
<title>acl_rules_53bd8961</title>
<polygon fill="#f2e6c2" stroke="none" points="1319,-731.08 1319,-752.08 1424,-752.08 1424,-731.08 1319,-731.08"/>
<text text-anchor="start" x="1321" y="-737.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">acl_rules</text>
<polygon fill="#f2e6c2" stroke="none" points="1424,-731.08 1424,-752.08 1570,-752.08 1570,-731.08 1424,-731.08"/>
<text text-anchor="start" x="1523" y="-736.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1321" y="-716.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="1417.75" y="-715.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-715.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1417.75" y="-694.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-694.78" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="1321" y="-673.78" font-family="Helvetica,sans-Serif" font-size="14.00">permissions</text>
<text text-anchor="start" x="1417.75" y="-673.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-673.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1321" y="-652.78" font-family="Helvetica,sans-Serif" font-size="14.00">type</text>
<text text-anchor="start" x="1417.75" y="-652.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1425.75" y="-652.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1320.62" y="-631.78" font-family="Helvetica,sans-Serif" font-size="14.00">workspace_id</text>
<text text-anchor="start" x="1417.75" y="-631.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-631.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="1321" y="-610.78" font-family="Helvetica,sans-Serif" font-size="14.00">org_id</text>
<text text-anchor="start" x="1417.75" y="-610.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-610.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="1321" y="-589.78" font-family="Helvetica,sans-Serif" font-size="14.00">doc_id</text>
<text text-anchor="start" x="1417.75" y="-589.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-589.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="1321" y="-568.78" font-family="Helvetica,sans-Serif" font-size="14.00">group_id</text>
<text text-anchor="start" x="1417.75" y="-568.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1426" y="-568.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<polygon fill="none" stroke="#888888" points="1318,-562.08 1318,-753.08 1571,-753.08 1571,-562.08 1318,-562.08"/>
</g>
<!-- docs_2f969a -->
<g id="node3" class="node">
<title>docs_2f969a</title>
<polygon fill="#f2e6c2" stroke="none" points="976,-535.08 976,-556.08 1117,-556.08 1117,-535.08 976,-535.08"/>
<text text-anchor="start" x="978" y="-541.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">docs</text>
<polygon fill="#f2e6c2" stroke="none" points="1117,-535.08 1117,-556.08 1265,-556.08 1265,-535.08 1117,-535.08"/>
<text text-anchor="start" x="1218" y="-540.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="978" y="-520.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="1110.75" y="-519.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-519.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="978" y="-498.78" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="1110.75" y="-498.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-498.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="978" y="-477.78" font-family="Helvetica,sans-Serif" font-size="14.00">created_at</text>
<text text-anchor="start" x="1110.75" y="-477.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1118.62" y="-477.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="978" y="-456.78" font-family="Helvetica,sans-Serif" font-size="14.00">updated_at</text>
<text text-anchor="start" x="1110.75" y="-456.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1118.62" y="-456.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="978" y="-435.78" font-family="Helvetica,sans-Serif" font-size="14.00">workspace_id</text>
<text text-anchor="start" x="1110.75" y="-435.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-435.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="978" y="-414.78" font-family="Helvetica,sans-Serif" font-size="14.00">is_pinned</text>
<text text-anchor="start" x="1110.75" y="-414.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-414.78" font-family="Helvetica,sans-Serif" font-size="14.00">BOOLEAN NOT NULL</text>
<text text-anchor="start" x="978" y="-393.78" font-family="Helvetica,sans-Serif" font-size="14.00">url_id</text>
<text text-anchor="start" x="1110.75" y="-393.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-393.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="978" y="-372.78" font-family="Helvetica,sans-Serif" font-size="14.00">removed_at</text>
<text text-anchor="start" x="1110.75" y="-372.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-372.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME</text>
<text text-anchor="start" x="978" y="-351.78" font-family="Helvetica,sans-Serif" font-size="14.00">options</text>
<text text-anchor="start" x="1110.75" y="-351.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-351.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="978" y="-330.78" font-family="Helvetica,sans-Serif" font-size="14.00">grace_period_start</text>
<text text-anchor="start" x="1110.75" y="-330.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-330.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME</text>
<text text-anchor="start" x="978" y="-309.78" font-family="Helvetica,sans-Serif" font-size="14.00">usage</text>
<text text-anchor="start" x="1110.75" y="-309.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-309.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="978" y="-288.78" font-family="Helvetica,sans-Serif" font-size="14.00">created_by</text>
<text text-anchor="start" x="1110.75" y="-288.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-288.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="978" y="-267.78" font-family="Helvetica,sans-Serif" font-size="14.00">trunk_id</text>
<text text-anchor="start" x="1110.75" y="-267.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-267.78" font-family="Helvetica,sans-Serif" font-size="14.00">TEXT</text>
<text text-anchor="start" x="978" y="-246.78" font-family="Helvetica,sans-Serif" font-size="14.00">type</text>
<text text-anchor="start" x="1110.75" y="-246.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1119" y="-246.78" font-family="Helvetica,sans-Serif" font-size="14.00">TEXT</text>
<polygon fill="none" stroke="#888888" points="975,-240.08 975,-557.08 1266,-557.08 1266,-240.08 975,-240.08"/>
</g>
<!-- acl_rules_53bd8961&#45;&gt;docs_2f969a -->
<g id="edge2" class="edge">
<title>acl_rules_53bd8961:w&#45;&gt;docs_2f969a:e</title>
<path fill="none" stroke="black" d="M1300.15,-586C1288.9,-569.65 1294.6,-534.89 1276.05,-526.46"/>
<polygon fill="black" stroke="black" points="1308.24,-590 1315.21,-598.47 1313.02,-592.37 1316.9,-594.29 1316.9,-594.29 1316.9,-594.29 1313.02,-592.37 1319.2,-590.4 1308.24,-590"/>
<ellipse fill="none" stroke="black" cx="1303.11" cy="-587.47" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1266.52,-529.68 1268.44,-519.87 1270.41,-520.25 1268.48,-530.07 1266.52,-529.68"/>
<polyline fill="none" stroke="black" points="1266.5,-524.58 1271.41,-525.55"/>
<polygon fill="black" stroke="black" points="1271.43,-530.64 1273.35,-520.83 1275.31,-521.22 1273.39,-531.03 1271.43,-530.64"/>
<polyline fill="none" stroke="black" points="1271.41,-525.55 1276.31,-526.51"/>
</g>
<!-- groups_b63e4e33 -->
<g id="node8" class="node">
<title>groups_b63e4e33</title>
<polygon fill="#f2e6c2" stroke="none" points="1018.5,-191.58 1018.5,-212.58 1076.5,-212.58 1076.5,-191.58 1018.5,-191.58"/>
<text text-anchor="start" x="1020.5" y="-198.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">groups</text>
<polygon fill="#f2e6c2" stroke="none" points="1076.5,-191.58 1076.5,-212.58 1222.5,-212.58 1222.5,-191.58 1076.5,-191.58"/>
<text text-anchor="start" x="1175.5" y="-197.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1020.5" y="-177.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="1066.5" y="-176.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1078.5" y="-176.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1066.5" y="-155.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1078.5" y="-155.28" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="1020.5" y="-134.28" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="1066.5" y="-134.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1078.25" y="-134.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1017.5,-127.58 1017.5,-213.58 1223.5,-213.58 1223.5,-127.58 1017.5,-127.58"/>
</g>
<!-- acl_rules_53bd8961&#45;&gt;groups_b63e4e33 -->
<g id="edge6" class="edge">
<title>acl_rules_53bd8961:w&#45;&gt;groups_b63e4e33:e</title>
<path fill="none" stroke="black" d="M1299.63,-566.93C1251.73,-524.31 1306.41,-289.28 1274,-226.58 1261.87,-203.11 1255.27,-186.14 1233.44,-182.37"/>
<polygon fill="black" stroke="black" points="1307.81,-569.98 1315.62,-577.68 1312.81,-571.84 1316.87,-573.35 1316.87,-573.35 1316.87,-573.35 1312.81,-571.84 1318.76,-569.25 1307.81,-569.98"/>
<ellipse fill="none" stroke="black" cx="1302.46" cy="-567.98" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1224.1,-186.65 1224.89,-176.68 1226.89,-176.84 1226.1,-186.81 1224.1,-186.65"/>
<polyline fill="none" stroke="black" points="1223.5,-181.58 1228.48,-181.98"/>
<polygon fill="black" stroke="black" points="1229.09,-187.04 1229.88,-177.07 1231.87,-177.23 1231.08,-187.2 1229.09,-187.04"/>
<polyline fill="none" stroke="black" points="1228.48,-181.98 1233.47,-182.37"/>
</g>
<!-- orgs_34a26e -->
<g id="node10" class="node">
<title>orgs_34a26e</title>
<polygon fill="#f2e6c2" stroke="none" points="341,-681.58 341,-702.58 476,-702.58 476,-681.58 341,-681.58"/>
<text text-anchor="start" x="343" y="-688.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">orgs</text>
<polygon fill="#f2e6c2" stroke="none" points="476,-681.58 476,-702.58 624,-702.58 624,-681.58 476,-681.58"/>
<text text-anchor="start" x="577" y="-687.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="343" y="-667.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="469.75" y="-666.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-666.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="469.75" y="-645.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-645.28" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="343" y="-624.28" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="469.75" y="-624.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-624.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="343" y="-603.28" font-family="Helvetica,sans-Serif" font-size="14.00">domain</text>
<text text-anchor="start" x="469.75" y="-603.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-603.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="343" y="-582.28" font-family="Helvetica,sans-Serif" font-size="14.00">created_at</text>
<text text-anchor="start" x="469.75" y="-582.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="477.62" y="-582.28" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="343" y="-561.28" font-family="Helvetica,sans-Serif" font-size="14.00">updated_at</text>
<text text-anchor="start" x="469.75" y="-561.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="477.62" y="-561.28" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="343" y="-540.28" font-family="Helvetica,sans-Serif" font-size="14.00">owner_id</text>
<text text-anchor="start" x="469.75" y="-540.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-540.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="343" y="-519.28" font-family="Helvetica,sans-Serif" font-size="14.00">billing_account_id</text>
<text text-anchor="start" x="469.75" y="-519.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-519.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="343" y="-498.28" font-family="Helvetica,sans-Serif" font-size="14.00">host</text>
<text text-anchor="start" x="469.75" y="-498.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="478" y="-498.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<polygon fill="none" stroke="#888888" points="340,-491.58 340,-703.58 625,-703.58 625,-491.58 340,-491.58"/>
</g>
<!-- acl_rules_53bd8961&#45;&gt;orgs_34a26e -->
<g id="edge11" class="edge">
<title>acl_rules_53bd8961:w&#45;&gt;orgs_34a26e:e</title>
<path fill="none" stroke="black" d="M1298.17,-615.35C1142.54,-611.87 795.65,-571.59 669,-626.58 647.07,-636.11 651.25,-662.5 635.2,-669.73"/>
<polygon fill="black" stroke="black" points="1307.17,-615.46 1317.11,-620.08 1312.5,-615.52 1316.83,-615.58 1316.83,-615.58 1316.83,-615.58 1312.5,-615.52 1317.22,-611.08 1307.17,-615.46"/>
<ellipse fill="none" stroke="black" cx="1301.45" cy="-615.39" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="627.42,-676.31 625.54,-666.49 627.51,-666.11 629.39,-675.93 627.42,-676.31"/>
<polyline fill="none" stroke="black" points="625.5,-671.58 630.41,-670.65"/>
<polygon fill="black" stroke="black" points="632.33,-675.37 630.45,-665.55 632.42,-665.17 634.3,-674.99 632.33,-675.37"/>
<polyline fill="none" stroke="black" points="630.41,-670.65 635.32,-669.71"/>
</g>
<!-- workspaces_e61add -->
<g id="node13" class="node">
<title>workspaces_e61add</title>
<polygon fill="#f2e6c2" stroke="none" points="678,-787.58 678,-808.58 774,-808.58 774,-787.58 678,-787.58"/>
<text text-anchor="start" x="679.88" y="-794.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">workspaces</text>
<polygon fill="#f2e6c2" stroke="none" points="774,-787.58 774,-808.58 922,-808.58 922,-787.58 774,-787.58"/>
<text text-anchor="start" x="875" y="-793.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="680" y="-773.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="766" y="-772.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="776" y="-772.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="766" y="-751.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="776" y="-751.28" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="680" y="-730.28" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="766" y="-730.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="776" y="-730.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="680" y="-709.28" font-family="Helvetica,sans-Serif" font-size="14.00">created_at</text>
<text text-anchor="start" x="766" y="-709.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="775.62" y="-709.28" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="680" y="-688.28" font-family="Helvetica,sans-Serif" font-size="14.00">updated_at</text>
<text text-anchor="start" x="766" y="-688.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="775.62" y="-688.28" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<text text-anchor="start" x="680" y="-667.28" font-family="Helvetica,sans-Serif" font-size="14.00">org_id</text>
<text text-anchor="start" x="766" y="-667.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="776" y="-667.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="680" y="-646.28" font-family="Helvetica,sans-Serif" font-size="14.00">removed_at</text>
<text text-anchor="start" x="766" y="-646.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="776" y="-646.28" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME</text>
<polygon fill="none" stroke="#888888" points="677,-639.58 677,-809.58 923,-809.58 923,-639.58 677,-639.58"/>
</g>
<!-- acl_rules_53bd8961&#45;&gt;workspaces_e61add -->
<g id="edge21" class="edge">
<title>acl_rules_53bd8961:w&#45;&gt;workspaces_e61add:e</title>
<path fill="none" stroke="black" d="M1298.2,-637.11C1132.65,-646.28 1103.61,-772.76 932.84,-777.45"/>
<polygon fill="black" stroke="black" points="1307.17,-636.86 1317.29,-641.09 1312.5,-636.72 1316.83,-636.6 1316.83,-636.6 1316.83,-636.6 1312.5,-636.72 1317.04,-632.09 1307.17,-636.86"/>
<ellipse fill="none" stroke="black" cx="1301.45" cy="-637.02" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="924.07,-782.57 923.93,-772.57 925.93,-772.54 926.07,-782.54 924.07,-782.57"/>
<polyline fill="none" stroke="black" points="923,-777.58 928,-777.52"/>
<polygon fill="black" stroke="black" points="929.07,-782.5 928.93,-772.5 930.93,-772.48 931.07,-782.47 929.07,-782.5"/>
<polyline fill="none" stroke="black" points="928,-777.52 933,-777.45"/>
</g>
<!-- aliases_c97dc35d -->
<g id="node2" class="node">
<title>aliases_c97dc35d</title>
<polygon fill="#f2e6c2" stroke="none" points="1328.5,-869.08 1328.5,-890.08 1412.5,-890.08 1412.5,-869.08 1328.5,-869.08"/>
<text text-anchor="start" x="1330.5" y="-875.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">aliases</text>
<polygon fill="#f2e6c2" stroke="none" points="1412.5,-869.08 1412.5,-890.08 1560.5,-890.08 1560.5,-869.08 1412.5,-869.08"/>
<text text-anchor="start" x="1513.5" y="-874.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1330.5" y="-854.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">url_id</text>
<text text-anchor="start" x="1406.25" y="-853.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1414.5" y="-853.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1330.5" y="-833.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">org_id</text>
<text text-anchor="start" x="1406.25" y="-832.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1414.5" y="-832.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1330.5" y="-811.78" font-family="Helvetica,sans-Serif" font-size="14.00">doc_id</text>
<text text-anchor="start" x="1406.25" y="-811.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1414.5" y="-811.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="1330.5" y="-790.78" font-family="Helvetica,sans-Serif" font-size="14.00">created_at</text>
<text text-anchor="start" x="1406.25" y="-790.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1414.12" y="-790.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1327.5,-784.08 1327.5,-891.08 1561.5,-891.08 1561.5,-784.08 1327.5,-784.08"/>
</g>
<!-- aliases_c97dc35d&#45;&gt;docs_2f969a -->
<g id="edge3" class="edge">
<title>aliases_c97dc35d:w&#45;&gt;docs_2f969a:e</title>
<path fill="none" stroke="black" d="M1310.57,-807.2C1277.15,-760.25 1319.85,-552.09 1276.08,-527.05"/>
<polygon fill="black" stroke="black" points="1318.46,-811.57 1325.03,-820.36 1323.13,-814.16 1326.91,-816.26 1326.91,-816.26 1326.91,-816.26 1323.13,-814.16 1329.39,-812.49 1318.46,-811.57"/>
<ellipse fill="none" stroke="black" cx="1313.46" cy="-808.8" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1266.22,-529.68 1268.71,-519.99 1270.65,-520.49 1268.16,-530.17 1266.22,-529.68"/>
<polyline fill="none" stroke="black" points="1266.5,-524.58 1271.34,-525.83"/>
<polygon fill="black" stroke="black" points="1271.06,-530.92 1273.56,-521.24 1275.49,-521.74 1273,-531.42 1271.06,-530.92"/>
<polyline fill="none" stroke="black" points="1271.34,-525.83 1276.18,-527.08"/>
</g>
<!-- aliases_c97dc35d&#45;&gt;orgs_34a26e -->
<g id="edge12" class="edge">
<title>aliases_c97dc35d:w&#45;&gt;orgs_34a26e:e</title>
<path fill="none" stroke="black" d="M1308.43,-837.82C1028.83,-844.61 895.79,-997.34 669,-822.58 616.6,-782.2 687.11,-682.83 635.33,-672.46"/>
<polygon fill="black" stroke="black" points="1317.17,-837.71 1327.22,-842.09 1322.5,-837.64 1326.83,-837.59 1326.83,-837.59 1326.83,-837.59 1322.5,-837.64 1327.11,-833.09 1317.17,-837.71"/>
<ellipse fill="none" stroke="black" cx="1311.45" cy="-837.78" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="626.05,-676.65 626.94,-666.69 628.93,-666.87 628.04,-676.83 626.05,-676.65"/>
<polyline fill="none" stroke="black" points="625.5,-671.58 630.48,-672.03"/>
<polygon fill="black" stroke="black" points="631.03,-677.1 631.92,-667.14 633.91,-667.32 633.02,-677.28 631.03,-677.1"/>
<polyline fill="none" stroke="black" points="630.48,-672.03 635.46,-672.47"/>
</g>
<!-- docs_2f969a&#45;&gt;docs_2f969a -->
<g id="edge1" class="edge">
<title>docs_2f969a:w&#45;&gt;docs_2f969a:e</title>
<path fill="none" stroke="black" d="M961.45,-285.94C896.25,-362.58 909.45,-579.08 1120.5,-579.08 1337.27,-579.08 1345.31,-567.96 1274.08,-529.19"/>
<polygon fill="black" stroke="black" points="968.01,-279.7 978.36,-276.08 971.88,-276.03 975.02,-273.04 975.02,-273.04 975.02,-273.04 971.88,-276.03 972.16,-269.55 968.01,-279.7"/>
<ellipse fill="none" stroke="black" cx="963.87" cy="-283.65" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1264.02,-529.46 1268.75,-520.65 1270.51,-521.6 1265.78,-530.41 1264.02,-529.46"/>
<polyline fill="none" stroke="black" points="1265.5,-524.58 1269.91,-526.95"/>
<polygon fill="black" stroke="black" points="1268.42,-531.83 1273.15,-523.02 1274.91,-523.96 1270.18,-532.77 1268.42,-531.83"/>
<polyline fill="none" stroke="black" points="1269.91,-526.95 1274.31,-529.31"/>
</g>
<!-- docs_2f969a&#45;&gt;workspaces_e61add -->
<g id="edge22" class="edge">
<title>docs_2f969a:w&#45;&gt;workspaces_e61add:e</title>
<path fill="none" stroke="black" d="M957.27,-449.25C918.44,-499.22 983.9,-749.14 932.68,-775.36"/>
<polygon fill="black" stroke="black" points="965.27,-445.23 976.22,-444.75 970.03,-442.83 973.9,-440.88 973.9,-440.88 973.9,-440.88 970.03,-442.83 972.18,-436.71 965.27,-445.23"/>
<ellipse fill="none" stroke="black" cx="960.16" cy="-447.8" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="925.1,-782.23 922.85,-772.49 924.8,-772.04 927.04,-781.78 925.1,-782.23"/>
<polyline fill="none" stroke="black" points="923,-777.58 927.87,-776.46"/>
<polygon fill="black" stroke="black" points="929.97,-781.11 927.73,-771.37 929.68,-770.92 931.92,-780.66 929.97,-781.11"/>
<polyline fill="none" stroke="black" points="927.87,-776.46 932.75,-775.34"/>
</g>
<!-- users_6a70267 -->
<g id="node14" class="node">
<title>users_6a70267</title>
<polygon fill="#f2e6c2" stroke="none" points="9,-293.08 9,-314.08 141,-314.08 141,-293.08 9,-293.08"/>
<text text-anchor="start" x="11" y="-299.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">users</text>
<polygon fill="#f2e6c2" stroke="none" points="141,-293.08 141,-314.08 287,-314.08 287,-293.08 141,-293.08"/>
<text text-anchor="start" x="240" y="-298.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="11" y="-278.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="134.75" y="-277.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-277.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="134.75" y="-256.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-256.78" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="11" y="-235.78" font-family="Helvetica,sans-Serif" font-size="14.00">name</text>
<text text-anchor="start" x="134.75" y="-235.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="142.75" y="-235.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="11" y="-214.78" font-family="Helvetica,sans-Serif" font-size="14.00">api_key</text>
<text text-anchor="start" x="134.75" y="-214.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-214.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="11" y="-193.78" font-family="Helvetica,sans-Serif" font-size="14.00">picture</text>
<text text-anchor="start" x="134.75" y="-193.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-193.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="11" y="-172.78" font-family="Helvetica,sans-Serif" font-size="14.00">first_login_at</text>
<text text-anchor="start" x="134.75" y="-172.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-172.78" font-family="Helvetica,sans-Serif" font-size="14.00">DATETIME</text>
<text text-anchor="start" x="10.62" y="-151.78" font-family="Helvetica,sans-Serif" font-size="14.00">is_first_time_user</text>
<text text-anchor="start" x="134.75" y="-151.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-151.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="11" y="-130.78" font-family="Helvetica,sans-Serif" font-size="14.00">options</text>
<text text-anchor="start" x="134.75" y="-130.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-130.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="11" y="-109.78" font-family="Helvetica,sans-Serif" font-size="14.00">connect_id</text>
<text text-anchor="start" x="134.75" y="-109.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="143" y="-109.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR</text>
<text text-anchor="start" x="11" y="-88.78" font-family="Helvetica,sans-Serif" font-size="14.00">&quot;ref&quot;</text>
<text text-anchor="start" x="134.75" y="-88.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="142.75" y="-88.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="8,-82.08 8,-315.08 288,-315.08 288,-82.08 8,-82.08"/>
</g>
<!-- docs_2f969a&#45;&gt;users_6a70267 -->
<g id="edge16" class="edge">
<title>docs_2f969a:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M955.27,-293.33C817.2,-289.57 779.98,-245.61 633,-232.58 499.74,-220.78 453.9,-177.48 332,-232.58 308.61,-243.16 314.96,-272.99 297.75,-280.73"/>
<polygon fill="black" stroke="black" points="964.17,-293.45 974.11,-298.08 969.5,-293.52 973.83,-293.58 973.83,-293.58 973.83,-293.58 969.5,-293.52 974.23,-289.08 964.17,-293.45"/>
<ellipse fill="none" stroke="black" cx="958.45" cy="-293.37" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="289.92,-287.31 288.05,-277.49 290.01,-277.11 291.88,-286.94 289.92,-287.31"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.91,-281.65"/>
<polygon fill="black" stroke="black" points="294.83,-286.38 292.96,-276.55 294.93,-276.18 296.79,-286 294.83,-286.38"/>
<polyline fill="none" stroke="black" points="292.91,-281.65 297.82,-280.72"/>
</g>
<!-- secrets_756efc22 -->
<g id="node4" class="node">
<title>secrets_756efc22</title>
<polygon fill="#f2e6c2" stroke="none" points="1340,-513.58 1340,-534.58 1403,-534.58 1403,-513.58 1340,-513.58"/>
<text text-anchor="start" x="1342" y="-520.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">secrets</text>
<polygon fill="#f2e6c2" stroke="none" points="1403,-513.58 1403,-534.58 1549,-534.58 1549,-513.58 1403,-513.58"/>
<text text-anchor="start" x="1502" y="-519.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1342" y="-499.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="1396.75" y="-498.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-498.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1342" y="-477.28" font-family="Helvetica,sans-Serif" font-size="14.00">&quot;value&quot;</text>
<text text-anchor="start" x="1396.75" y="-477.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-477.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1342" y="-456.28" font-family="Helvetica,sans-Serif" font-size="14.00">doc_id</text>
<text text-anchor="start" x="1396.75" y="-456.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-456.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1339,-449.58 1339,-535.58 1550,-535.58 1550,-449.58 1339,-449.58"/>
</g>
<!-- secrets_756efc22&#45;&gt;docs_2f969a -->
<g id="edge4" class="edge">
<title>secrets_756efc22:w&#45;&gt;docs_2f969a:e</title>
<path fill="none" stroke="black" d="M1319.93,-466.26C1301.02,-480.27 1301.16,-515.89 1276.38,-523.24"/>
<polygon fill="black" stroke="black" points="1328.62,-463.61 1339.5,-464.99 1333.72,-462.05 1337.86,-460.78 1337.86,-460.78 1337.86,-460.78 1333.72,-462.05 1336.86,-456.38 1328.62,-463.61"/>
<ellipse fill="none" stroke="black" cx="1323.15" cy="-465.28" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1268.16,-529.4 1266.82,-519.49 1268.8,-519.23 1270.15,-529.13 1268.16,-529.4"/>
<polyline fill="none" stroke="black" points="1266.5,-524.58 1271.45,-523.91"/>
<polygon fill="black" stroke="black" points="1273.12,-528.73 1271.77,-518.82 1273.75,-518.55 1275.1,-528.46 1273.12,-528.73"/>
<polyline fill="none" stroke="black" points="1271.45,-523.91 1276.41,-523.24"/>
</g>
<!-- shares_ca2520d3 -->
<g id="node5" class="node">
<title>shares_ca2520d3</title>
<polygon fill="#f2e6c2" stroke="none" points="1340,-401.08 1340,-422.08 1403,-422.08 1403,-401.08 1340,-401.08"/>
<text text-anchor="start" x="1342" y="-407.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">shares</text>
<polygon fill="#f2e6c2" stroke="none" points="1403,-401.08 1403,-422.08 1549,-422.08 1549,-401.08 1403,-401.08"/>
<text text-anchor="start" x="1502" y="-406.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1342" y="-386.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="1396.75" y="-385.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1405" y="-385.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1396.75" y="-364.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1405" y="-364.78" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="1342" y="-343.78" font-family="Helvetica,sans-Serif" font-size="14.00">key</text>
<text text-anchor="start" x="1396.75" y="-343.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-343.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1342" y="-322.78" font-family="Helvetica,sans-Serif" font-size="14.00">doc_id</text>
<text text-anchor="start" x="1396.75" y="-322.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-322.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1342" y="-301.78" font-family="Helvetica,sans-Serif" font-size="14.00">link_id</text>
<text text-anchor="start" x="1396.75" y="-301.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-301.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="1342" y="-280.78" font-family="Helvetica,sans-Serif" font-size="14.00">options</text>
<text text-anchor="start" x="1396.75" y="-280.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1404.75" y="-280.78" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1339,-274.08 1339,-423.08 1550,-423.08 1550,-274.08 1339,-274.08"/>
</g>
<!-- shares_ca2520d3&#45;&gt;docs_2f969a -->
<g id="edge5" class="edge">
<title>shares_ca2520d3:w&#45;&gt;docs_2f969a:e</title>
<path fill="none" stroke="black" d="M1319.52,-331.34C1264.99,-357.46 1344.98,-511.2 1276.17,-523.77"/>
<polygon fill="black" stroke="black" points="1328.36,-329.59 1339.05,-332.06 1333.6,-328.56 1337.84,-327.71 1337.84,-327.71 1337.84,-327.71 1333.6,-328.56 1337.3,-323.23 1328.36,-329.59"/>
<ellipse fill="none" stroke="black" cx="1322.75" cy="-330.7" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1267.92,-529.48 1267.08,-519.52 1269.07,-519.35 1269.91,-529.31 1267.92,-529.48"/>
<polyline fill="none" stroke="black" points="1266.5,-524.58 1271.48,-524.16"/>
<polygon fill="black" stroke="black" points="1272.9,-529.06 1272.06,-519.1 1274.05,-518.93 1274.89,-528.89 1272.9,-529.06"/>
<polyline fill="none" stroke="black" points="1271.48,-524.16 1276.46,-523.74"/>
</g>
<!-- group_groups_dfa1d7f3 -->
<g id="node6" class="node">
<title>group_groups_dfa1d7f3</title>
<polygon fill="#f2e6c2" stroke="none" points="1319.5,-215.08 1319.5,-236.08 1429.5,-236.08 1429.5,-215.08 1319.5,-215.08"/>
<text text-anchor="start" x="1321.25" y="-221.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">group_groups</text>
<polygon fill="#f2e6c2" stroke="none" points="1429.5,-215.08 1429.5,-236.08 1569.5,-236.08 1569.5,-215.08 1429.5,-215.08"/>
<text text-anchor="start" x="1522.5" y="-220.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1321.5" y="-200.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">group_id</text>
<text text-anchor="start" x="1421.5" y="-199.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1431.25" y="-199.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1321.5" y="-179.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">subgroup_id</text>
<text text-anchor="start" x="1421.5" y="-178.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1431.25" y="-178.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1318.5,-172.08 1318.5,-237.08 1570.5,-237.08 1570.5,-172.08 1318.5,-172.08"/>
</g>
<!-- group_groups_dfa1d7f3&#45;&gt;groups_b63e4e33 -->
<g id="edge7" class="edge">
<title>group_groups_dfa1d7f3:w&#45;&gt;groups_b63e4e33:e</title>
<path fill="none" stroke="black" d="M1299.26,-202.86C1274.06,-198.03 1262.08,-184.59 1233.43,-182.01"/>
<polygon fill="black" stroke="black" points="1308.21,-203.66 1317.76,-209.04 1313.52,-204.14 1317.83,-204.52 1317.83,-204.52 1317.83,-204.52 1313.52,-204.14 1318.57,-200.07 1308.21,-203.66"/>
<ellipse fill="none" stroke="black" cx="1302.51" cy="-203.15" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1224.28,-186.62 1224.72,-176.63 1226.71,-176.72 1226.28,-186.71 1224.28,-186.62"/>
<polyline fill="none" stroke="black" points="1223.5,-181.58 1228.5,-181.8"/>
<polygon fill="black" stroke="black" points="1229.28,-186.84 1229.71,-176.85 1231.71,-176.94 1231.28,-186.93 1229.28,-186.84"/>
<polyline fill="none" stroke="black" points="1228.5,-181.8 1233.49,-182.02"/>
</g>
<!-- group_groups_dfa1d7f3&#45;&gt;groups_b63e4e33 -->
<g id="edge8" class="edge">
<title>group_groups_dfa1d7f3:w&#45;&gt;groups_b63e4e33:e</title>
<path fill="none" stroke="black" d="M1299.21,-183.43C1274.4,-183 1261.19,-181.84 1233.16,-181.62"/>
<polygon fill="black" stroke="black" points="1308.17,-183.5 1318.13,-188.08 1313.5,-183.54 1317.83,-183.58 1317.83,-183.58 1317.83,-183.58 1313.5,-183.54 1318.2,-179.08 1308.17,-183.5"/>
<ellipse fill="none" stroke="black" cx="1302.45" cy="-183.45" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1224.48,-186.59 1224.52,-176.59 1226.52,-176.6 1226.48,-186.6 1224.48,-186.59"/>
<polyline fill="none" stroke="black" points="1223.5,-181.58 1228.5,-181.6"/>
<polygon fill="black" stroke="black" points="1229.48,-186.61 1229.52,-176.61 1231.52,-176.61 1231.48,-186.61 1229.48,-186.61"/>
<polyline fill="none" stroke="black" points="1228.5,-181.6 1233.5,-181.62"/>
</g>
<!-- group_users_41cb40a7 -->
<g id="node7" class="node">
<title>group_users_41cb40a7</title>
<polygon fill="#f2e6c2" stroke="none" points="1325,-124.08 1325,-145.08 1424,-145.08 1424,-124.08 1325,-124.08"/>
<text text-anchor="start" x="1326.88" y="-130.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">group_users</text>
<polygon fill="#f2e6c2" stroke="none" points="1424,-124.08 1424,-145.08 1564,-145.08 1564,-124.08 1424,-124.08"/>
<text text-anchor="start" x="1517" y="-129.78" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="1327" y="-109.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">group_id</text>
<text text-anchor="start" x="1407" y="-108.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1425.75" y="-108.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="1327" y="-88.78" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">user_id</text>
<text text-anchor="start" x="1407" y="-87.78" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="1425.75" y="-87.78" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<polygon fill="none" stroke="#888888" points="1324,-81.08 1324,-146.08 1565,-146.08 1565,-81.08 1324,-81.08"/>
</g>
<!-- group_users_41cb40a7&#45;&gt;groups_b63e4e33 -->
<g id="edge9" class="edge">
<title>group_users_41cb40a7:w&#45;&gt;groups_b63e4e33:e</title>
<path fill="none" stroke="black" d="M1304.58,-117.05C1273.18,-129.88 1270.19,-174.26 1233.11,-180.78"/>
<polygon fill="black" stroke="black" points="1313.33,-115.45 1323.98,-118.07 1318.58,-114.48 1322.84,-113.7 1322.84,-113.7 1322.84,-113.7 1318.58,-114.48 1322.36,-109.22 1313.33,-115.45"/>
<ellipse fill="none" stroke="black" cx="1307.71" cy="-116.48" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="1224.91,-186.48 1224.08,-176.52 1226.07,-176.35 1226.91,-186.32 1224.91,-186.48"/>
<polyline fill="none" stroke="black" points="1223.5,-181.58 1228.48,-181.17"/>
<polygon fill="black" stroke="black" points="1229.89,-186.07 1229.06,-176.1 1231.06,-175.94 1231.89,-185.9 1229.89,-186.07"/>
<polyline fill="none" stroke="black" points="1228.48,-181.17 1233.47,-180.75"/>
</g>
<!-- group_users_41cb40a7&#45;&gt;users_6a70267 -->
<g id="edge17" class="edge">
<title>group_users_41cb40a7:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M1304.18,-92.49C874.76,-88.48 691.37,46.75 332,-201.58 301.46,-222.69 323.78,-272.07 297.75,-281.14"/>
<polygon fill="black" stroke="black" points="1313.17,-92.54 1323.14,-97.08 1318.5,-92.56 1322.83,-92.58 1322.83,-92.58 1322.83,-92.58 1318.5,-92.56 1323.19,-88.08 1313.17,-92.54"/>
<ellipse fill="none" stroke="black" cx="1307.45" cy="-92.51" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="289.72,-287.38 288.26,-277.49 290.23,-277.2 291.7,-287.09 289.72,-287.38"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.95,-281.85"/>
<polygon fill="black" stroke="black" points="294.67,-286.65 293.2,-276.76 295.18,-276.46 296.65,-286.36 294.67,-286.65"/>
<polyline fill="none" stroke="black" points="292.95,-281.85 297.89,-281.12"/>
</g>
<!-- logins_be987289 -->
<g id="node9" class="node">
<title>logins_be987289</title>
<polygon fill="#f2e6c2" stroke="none" points="357,-443.58 357,-464.58 462,-464.58 462,-443.58 357,-443.58"/>
<text text-anchor="start" x="359" y="-450.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">logins</text>
<polygon fill="#f2e6c2" stroke="none" points="462,-443.58 462,-464.58 608,-464.58 608,-443.58 462,-443.58"/>
<text text-anchor="start" x="561" y="-449.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="359" y="-429.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">id</text>
<text text-anchor="start" x="455.75" y="-428.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="464" y="-428.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="455.75" y="-407.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="464" y="-407.28" font-family="Helvetica,sans-Serif" font-size="14.00">auto&#45;incremented</text>
<text text-anchor="start" x="359" y="-386.28" font-family="Helvetica,sans-Serif" font-size="14.00">user_id</text>
<text text-anchor="start" x="455.75" y="-386.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="464" y="-386.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER NOT NULL</text>
<text text-anchor="start" x="359" y="-365.28" font-family="Helvetica,sans-Serif" font-size="14.00">email</text>
<text text-anchor="start" x="455.75" y="-365.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="463.75" y="-365.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<text text-anchor="start" x="358.62" y="-344.28" font-family="Helvetica,sans-Serif" font-size="14.00">display_email</text>
<text text-anchor="start" x="455.75" y="-344.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="463.75" y="-344.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="356,-337.58 356,-465.58 609,-465.58 609,-337.58 356,-337.58"/>
</g>
<!-- logins_be987289&#45;&gt;users_6a70267 -->
<g id="edge18" class="edge">
<title>logins_be987289:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M337.12,-384.78C311.76,-364.01 332.95,-294.11 297.64,-283.85"/>
<polygon fill="black" stroke="black" points="345.65,-387.47 353.83,-394.77 350.73,-389.08 354.86,-390.38 354.86,-390.38 354.86,-390.38 350.73,-389.08 356.54,-386.19 345.65,-387.47"/>
<ellipse fill="none" stroke="black" cx="340.19" cy="-385.75" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="288.34,-287.67 289.64,-277.76 291.62,-278.02 290.33,-287.93 288.34,-287.67"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.96,-283.23"/>
<polygon fill="black" stroke="black" points="293.3,-288.32 294.6,-278.41 296.58,-278.67 295.28,-288.58 293.3,-288.32"/>
<polyline fill="none" stroke="black" points="292.96,-283.23 297.92,-283.88"/>
</g>
<!-- id_dc7c64b2 -->
<g id="node11" class="node">
<title>id_dc7c64b2</title>
<text text-anchor="start" x="83.88" y="-518.91" font-family="Helvetica,sans-Serif" font-size="14.00">billing_accounts.id</text>
</g>
<!-- orgs_34a26e&#45;&gt;id_dc7c64b2 -->
<g id="edge10" class="edge">
<title>orgs_34a26e:w&#45;&gt;id_dc7c64b2:e</title>
<path fill="none" stroke="black" d="M320.17,-523.58C285.36,-523.58 269.06,-523.58 230.68,-523.58"/>
<polygon fill="black" stroke="black" points="329.17,-523.58 339.17,-528.08 334.5,-523.58 338.83,-523.58 338.83,-523.58 338.83,-523.58 334.5,-523.58 339.17,-519.08 329.17,-523.58"/>
<ellipse fill="none" stroke="black" cx="323.45" cy="-523.58" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="222,-528.58 222,-518.58 224,-518.58 224,-528.58 222,-528.58"/>
<polyline fill="none" stroke="black" points="221,-523.58 226,-523.58"/>
<polygon fill="black" stroke="black" points="227,-528.58 227,-518.58 229,-518.58 229,-528.58 227,-528.58"/>
<polyline fill="none" stroke="black" points="226,-523.58 231,-523.58"/>
</g>
<!-- orgs_34a26e&#45;&gt;users_6a70267 -->
<g id="edge19" class="edge">
<title>orgs_34a26e:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M325.84,-543.17C239.04,-523.2 391.49,-297.37 297.85,-283.27"/>
<polygon fill="black" stroke="black" points="339.02,-539.51 337.99,-549.45 336,-549.25 337.03,-539.3 339.02,-539.51"/>
<polyline fill="none" stroke="black" points="339.5,-544.58 334.53,-544.07"/>
<ellipse fill="none" stroke="black" cx="330.05" cy="-543.61" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="288.65,-287.64 289.35,-277.67 291.34,-277.81 290.64,-287.78 288.65,-287.64"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.99,-282.93"/>
<polygon fill="black" stroke="black" points="293.64,-287.99 294.33,-278.02 296.33,-278.15 295.63,-288.13 293.64,-287.99"/>
<polyline fill="none" stroke="black" points="292.99,-282.93 297.98,-283.28"/>
</g>
<!-- prefs_660170f -->
<g id="node12" class="node">
<title>prefs_660170f</title>
<polygon fill="#f2e6c2" stroke="none" points="696,-445.58 696,-466.58 758,-466.58 758,-445.58 696,-445.58"/>
<text text-anchor="start" x="698" y="-452.28" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">prefs</text>
<polygon fill="#f2e6c2" stroke="none" points="758,-445.58 758,-466.58 904,-466.58 904,-445.58 758,-445.58"/>
<text text-anchor="start" x="857" y="-451.28" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text>
<text text-anchor="start" x="698" y="-430.28" font-family="Helvetica,sans-Serif" font-size="14.00">org_id</text>
<text text-anchor="start" x="751.75" y="-430.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="760" y="-430.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="697.88" y="-409.28" font-family="Helvetica,sans-Serif" font-size="14.00">user_id</text>
<text text-anchor="start" x="751.75" y="-409.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="760" y="-409.28" font-family="Helvetica,sans-Serif" font-size="14.00">INTEGER</text>
<text text-anchor="start" x="698" y="-388.28" font-family="Helvetica,sans-Serif" font-size="14.00">prefs</text>
<text text-anchor="start" x="751.75" y="-388.28" font-family="Helvetica,sans-Serif" font-size="14.00"> </text>
<text text-anchor="start" x="759.75" y="-388.28" font-family="Helvetica,sans-Serif" font-size="14.00">VARCHAR NOT NULL</text>
<polygon fill="none" stroke="#888888" points="695,-381.58 695,-467.58 905,-467.58 905,-381.58 695,-381.58"/>
</g>
<!-- prefs_660170f&#45;&gt;orgs_34a26e -->
<g id="edge13" class="edge">
<title>prefs_660170f:w&#45;&gt;orgs_34a26e:e</title>
<path fill="none" stroke="black" d="M676.04,-438.74C607.35,-465.76 718.99,-657.4 635.43,-670.84"/>
<polygon fill="black" stroke="black" points="684.81,-437.28 695.41,-440.08 690.07,-436.41 694.34,-435.69 694.34,-435.69 694.34,-435.69 690.07,-436.41 693.93,-431.2 684.81,-437.28"/>
<ellipse fill="none" stroke="black" cx="679.17" cy="-438.22" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="626.87,-676.5 626.12,-666.52 628.12,-666.37 628.86,-676.35 626.87,-676.5"/>
<polyline fill="none" stroke="black" points="625.5,-671.58 630.49,-671.21"/>
<polygon fill="black" stroke="black" points="631.86,-676.12 631.11,-666.15 633.1,-666 633.85,-675.97 631.86,-676.12"/>
<polyline fill="none" stroke="black" points="630.49,-671.21 635.47,-670.84"/>
</g>
<!-- prefs_660170f&#45;&gt;users_6a70267 -->
<g id="edge20" class="edge">
<title>prefs_660170f:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M676.43,-408.64C654.09,-393.14 667.37,-345.27 633,-324.58 625.47,-320.05 353.85,-288.98 297.83,-283.43"/>
<polygon fill="black" stroke="black" points="685.01,-410.93 693.52,-417.85 690.17,-412.3 694.35,-413.41 694.35,-413.41 694.35,-413.41 690.17,-412.3 695.83,-409.15 685.01,-410.93"/>
<ellipse fill="none" stroke="black" cx="679.49" cy="-409.46" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="288.57,-287.65 289.43,-277.69 291.42,-277.86 290.56,-287.82 288.57,-287.65"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.98,-283.01"/>
<polygon fill="black" stroke="black" points="293.55,-288.08 294.41,-278.12 296.4,-278.29 295.54,-288.25 293.55,-288.08"/>
<polyline fill="none" stroke="black" points="292.98,-283.01 297.96,-283.44"/>
</g>
<!-- workspaces_e61add&#45;&gt;orgs_34a26e -->
<g id="edge14" class="edge">
<title>workspaces_e61add:w&#45;&gt;orgs_34a26e:e</title>
<path fill="none" stroke="black" d="M657.73,-671.58C650.41,-671.58 644.29,-671.58 635.42,-671.58"/>
<polygon fill="black" stroke="black" points="666.67,-671.58 676.67,-676.08 672,-671.58 676.33,-671.58 676.33,-671.58 676.33,-671.58 672,-671.58 676.67,-667.08 666.67,-671.58"/>
<ellipse fill="none" stroke="black" cx="660.95" cy="-671.58" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="626.5,-676.58 626.5,-666.58 628.5,-666.58 628.5,-676.58 626.5,-676.58"/>
<polyline fill="none" stroke="black" points="625.5,-671.58 630.5,-671.58"/>
<polygon fill="black" stroke="black" points="631.5,-676.58 631.5,-666.58 633.5,-666.58 633.5,-676.58 631.5,-676.58"/>
<polyline fill="none" stroke="black" points="630.5,-671.58 635.5,-671.58"/>
</g>
<!-- user_id_2d5fdf94 -->
<g id="node15" class="node">
<title>user_id_2d5fdf94</title>
<text text-anchor="start" x="365.12" y="-254.91" font-family="Helvetica,sans-Serif" font-size="14.00">billing_account_managers.user_id</text>
</g>
<!-- user_id_2d5fdf94&#45;&gt;users_6a70267 -->
<g id="edge15" class="edge">
<title>user_id_2d5fdf94:w&#45;&gt;users_6a70267:e</title>
<path fill="none" stroke="black" d="M337.53,-262.88C323.26,-268.29 315.11,-278.78 297.91,-281.77"/>
<polygon fill="black" stroke="black" points="346.32,-261.35 356.94,-264.07 351.57,-260.44 355.84,-259.7 355.84,-259.7 355.84,-259.7 351.57,-260.44 355.4,-255.21 346.32,-261.35"/>
<ellipse fill="none" stroke="black" cx="340.68" cy="-262.33" rx="4" ry="4"/>
<polygon fill="black" stroke="black" points="289.41,-287.49 288.59,-277.52 290.58,-277.35 291.4,-287.32 289.41,-287.49"/>
<polyline fill="none" stroke="black" points="288,-282.58 292.98,-282.17"/>
<polygon fill="black" stroke="black" points="294.39,-287.07 293.57,-277.11 295.56,-276.94 296.38,-286.91 294.39,-287.07"/>
<polyline fill="none" stroke="black" points="292.98,-282.17 297.97,-281.76"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@ -0,0 +1,175 @@
# This repository adheres to the publiccode.yml standard by including this
# metadata file that makes public software easily discoverable.
# More info at https://github.com/italia/publiccode.yml
publiccodeYmlVersion: '0.2'
categories:
- data-collection
- crm
- compliance-management
- office
dependsOn:
open:
- name: NodeJS
optional: false
version: ''
versionMax: ''
versionMin: '18'
- name: Python
optional: false
version: ''
versionMax: ''
versionMin: '3.9'
- name: Yarn
optional: true
version: ''
versionMax: ''
versionMin: ''
- name: Postgresql
optional: true
version: ''
versionMax: ''
versionMin: ''
- name: Redis
optional: true
version: ''
versionMax: ''
versionMin: ''
description:
en:
apiDocumentation: 'https://support.getgrist.com/api/'
documentation: 'https://support.getgrist.com/'
features:
- database
- spreadsheet
- low-code
- no-code
- form generation
- webhook
- calendar
- map
- python formulas
genericName: collaborative spreadsheet
longDescription: |
Grist is a hybrid database/spreadsheet, meaning that:
- Columns work like they do in databases: they are named, and they hold one kind of data.
- Columns can be filled by formula, spreadsheet-style, with automatic updates when referenced cells change.
This difference can confuse people coming directly from Excel or Google
Sheets. Give it a chance! There's also a [Grist for Spreadsheet
Users](https://www.getgrist.com/blog/grist-for-spreadsheet-users/) article
to help get you oriented. If you're coming from Airtable, you'll find the
model familiar (and there's also our [Grist vs
Airtable](https://www.getgrist.com/blog/grist-v-airtable/) article for a
direct comparison).
Here are some specific feature highlights of Grist:
- Python formulas.
- Full [Python syntax is supported](https://support.getgrist.com/formulas/#python), including the standard library.
- Many [Excel functions](https://support.getgrist.com/functions/) also available.
- An [AI Assistant](https://www.getgrist.com/ai-formula-assistant/) specifically tuned for formula generation (using OpenAI gpt-3.5-turbo or [Llama](https://ai.meta.com/llama/) via [llama-cpp-python](https://github.com/abetlen/llama-cpp-python)).
- A portable, self-contained format.
- Based on SQLite, the most widely deployed database engine.
- Any tool that can read SQLite can read numeric and text data from a Grist file.
- Enables [backups](https://support.getgrist.com/exports/#backing-up-an-entire-document) that you can confidently restore in full.
- Great for moving between different hosts.
- Can be displayed on a static website with [`grist-static`](https://github.com/gristlabs/grist-static) no special server needed.
- A self-contained desktop app for viewing and editing locally: [`grist-electron`](https://github.com/gristlabs/grist-electron).
- Convenient editing and formatting features.
- Choices and [choice lists](https://support.getgrist.com/col-types/#choice-list-columns), for adding colorful tags to records.
- [References](https://support.getgrist.com/col-refs/#creating-a-new-reference-list-column) and reference lists, for cross-referencing records in other tables.
- [Attachments](https://support.getgrist.com/col-types/#attachment-columns), to include media or document files in records.
- Dates and times, toggles, and special numerics such as currency all have specialized editors and formatting options.
- [Conditional Formatting](https://support.getgrist.com/conditional-formatting/), letting you control the style of cells with formulas to draw attention to important information.
- Drag-and-drop dashboards.
- [Charts](https://support.getgrist.com/widget-chart/), [card views](https://support.getgrist.com/widget-card/) and a [calendar widget](https://support.getgrist.com/widget-calendar/) for visualization.
- [Summary tables](https://support.getgrist.com/summary-tables/) for summing and counting across groups.
- [Widget linking](https://support.getgrist.com/linking-widgets/) streamlines filtering and editing data. Grist has a unique approach to visualization, where you can lay out and link distinct widgets to show together, without cramming mixed material into a table.
- [Filter bar](https://support.getgrist.com/search-sort-filter/#filter-buttons) for quick slicing and dicing.
- [Incremental imports](https://support.getgrist.com/imports/#updating-existing-records).
- Import a CSV of the last three months activity from your bank...
- ...and import new activity a month later without fuss or duplication.
- Integrations.
- A [REST API](https://support.getgrist.com/api/), [Zapier actions/triggers](https://support.getgrist.com/integrators/#integrations-via-zapier), and support from similar [integrators](https://support.getgrist.com/integrators/).
- Import/export to Google drive, Excel format, CSV.
- Link data with [custom widgets](https://support.getgrist.com/widget-custom/#_top), hosted externally.
- Configurable outgoing webhooks.
- [Many templates](https://templates.getgrist.com/) to get you started, from investment research to organizing treasure hunts.
- Access control options.
- (You'll need SSO logins set up to make use of these options; [`grist-omnibus`](https://github.com/gristlabs/grist-omnibus) has a prepackaged solution if configuring this feels daunting)
- Share [individual documents](https://support.getgrist.com/sharing/), workspaces, or [team sites](https://support.getgrist.com/team-sharing/).
- Control access to [individual rows, columns, and tables](https://support.getgrist.com/access-rules/).
- Control access based on cell values and user attributes.
- Self-maintainable.
- Useful for intranet operation and specific compliance requirements.
- Sandboxing options for untrusted documents.
- On Linux or with Docker, you can enable [gVisor](https://github.com/google/gvisor) sandboxing at the individual document level.
- On macOS, you can use native sandboxing.
- On any OS, including Windows, you can use a wasm-based sandbox.
- Translated to many languages.
- `F1` key brings up some quick help. This used to go without saying, but in general Grist has good keyboard support.
shortDescription: |-
Grist is a modern relational spreadsheet. It combines the flexibility of a
spreadsheet with the robustness of a database.
videos:
- 'https://www.youtube.com/watch?v=XYZ_ZGSxU00'
developmentStatus: stable
inputTypes:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- text/csv
it:
conforme:
gdpr: false
lineeGuidaDesign: false
misureMinimeSicurezza: false
modelloInteroperabilita: false
countryExtensionVersion: '0.2'
piattaforme:
anpr: false
cie: false
pagopa: false
spid: false
landingURL: 'https://getgrist.com'
legal:
license: Apache-2.0
localisation:
availableLanguages:
- en
- fr
- ru
- de
- es
- pt
- zh
- it
- ja
- 'no'
- ro
- sl
- uk
localisationReady: true
logo: |-
https://raw.githubusercontent.com/gristlabs/grist-core/master/static/img/logo-grist.png
maintenance:
contacts:
- affiliation: Grist Labs
email: paul@getgrist.com
name: Paul Fitzpatrick
type: internal
name: Grist
outputTypes:
- application/x-sqlite3
platforms:
- web
releaseDate: '2024-06-12'
roadmap: 'https://github.com/gristlabs/grist-core/projects/1'
softwareType: standalone/other
softwareVersion: 1.1.15
url: 'https://github.com/gristlabs/grist-core'
usedBy:
- 'ANCT (https://anct.gouv.fr)'
- 'DINUM (https://www.numerique.gouv.fr/dinum/)'

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Runs the command provided as arguments, but attempts to configure permissions first.
important_read_dirs=("/grist" "/persist")
write_dir="/persist"
current_user_id=$(id -u)
# We want to avoid running Grist as root if possible.
# Try to setup permissions and de-elevate to a normal user.
if [[ $current_user_id == 0 ]]; then
target_user=${GRIST_DOCKER_USER:-grist}
target_group=${GRIST_DOCKER_GROUP:-grist}
# Make sure the target user owns everything that Grist needs write access to.
find $write_dir ! -user "$target_user" -exec chown "$target_user" "{}" +
# Restart as the target user, replacing the current process (replacement is needed for security).
# Alternative tools to setpriv are: chroot, gosu.
# Need to use `exec` to close the parent shell, to avoid vulnerabilities: https://github.com/tianon/gosu/issues/37
exec setpriv --reuid "$target_user" --regid "$target_group" --init-groups /usr/bin/env bash "$0" "$@"
fi
# Validate that this user has access to the top level of each important directory.
# There might be a benefit to testing individual files, but this is simpler as the dir may start empty.
for dir in "${important_read_dirs[@]}"; do
if ! { test -r "$dir" ;} ; then
echo "Invalid permissions, cannot read '$dir'. Aborting." >&2
exit 1
fi
done
for dir in "${important_write_dirs[@]}"; do
if ! { test -r "$dir" && test -w "$dir" ;} ; then
echo "Invalid permissions, cannot write '$dir'. Aborting." >&2
exit 1
fi
done
exec /usr/bin/tini -s -- "$@"

@ -0,0 +1,35 @@
import {spawn} from 'child_process';
let grist;
function startGrist(newConfig={}) {
saveNewConfig(newConfig);
// H/T https://stackoverflow.com/a/36995148/11352427
grist = spawn('./sandbox/run.sh', {
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
});
grist.on('message', function(data) {
if (data.action === 'restart') {
console.log('Restarting Grist with new environment');
// Note that we only set this event handler here, after we have
// a new environment to reload with. Small chance of a race here
// in case something else sends a SIGINT before we do it
// ourselves further below.
grist.on('exit', () => {
grist = startGrist(data.newConfig);
});
grist.kill('SIGINT');
}
});
return grist;
}
// Stub function
function saveNewConfig(newConfig) {
// TODO: something here to actually persist the new config before
// restarting Grist.
}
startGrist();

@ -347,7 +347,9 @@
"Formula timer": "Formel Timer", "Formula timer": "Formel Timer",
"Cancel": "Abbrechen", "Cancel": "Abbrechen",
"Timing is on": "Das Timing läuft", "Timing is on": "Das Timing läuft",
"You can make changes to the document, then stop timing to see the results.": "Sie können Änderungen an dem Dokument vornehmen und dann die Zeitmessung stoppen, um die Ergebnisse zu sehen." "You can make changes to the document, then stop timing to see the results.": "Sie können Änderungen an dem Dokument vornehmen und dann die Zeitmessung stoppen, um die Ergebnisse zu sehen.",
"Only available to document editors": "Nur für Redakteure von Dokumenten verfügbar",
"Only available to document owners": "Nur für Eigentümer von Dokumenten verfügbar"
}, },
"DocumentUsage": { "DocumentUsage": {
"Attachments Size": "Größe der Anhänge", "Attachments Size": "Größe der Anhänge",
@ -1611,7 +1613,11 @@
"No fault detected.": "Kein Fehler erkannt.", "No fault detected.": "Kein Fehler erkannt.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird.", "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird.",
"Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "Als Ausweichmöglichkeit können Sie auch {{bootKey}} in der Umgebung einstellen und {{url}} besuchen", "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "Als Ausweichmöglichkeit können Sie auch {{bootKey}} in der Umgebung einstellen und {{url}} besuchen",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird." "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "In Grist können verschiedene Arten der Authentifizierung konfiguriert werden, darunter SAML und OIDC. Wir empfehlen, eine davon zu aktivieren, wenn Grist über das Netzwerk zugänglich ist oder mehreren Personen zur Verfügung gestellt wird.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist signiert die Sitzungscookies der Benutzer mit einem geheimen Schlüssel. Bitte setzen Sie diesen Schlüssel über die Umgebungsvariable GRIST_SESSION_SECRET. Grist greift auf eine hart kodierte Voreinstellung zurück, wenn sie nicht gesetzt ist. Wir werden diesen Hinweis möglicherweise in Zukunft entfernen, da Sitzungs-IDs, die seit v1.1.16 erzeugt werden, von Natur aus kryptographisch sicher sind.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist signiert die Sitzungscookies der Benutzer mit einem geheimen Schlüssel. Bitte setzen Sie diesen Schlüssel über die Umgebungsvariable GRIST_SESSION_SECRET. Grist greift auf eine hart kodierte Voreinstellung zurück, wenn sie nicht gesetzt ist. Wir werden diesen Hinweis möglicherweise in Zukunft entfernen, da Sitzungs-IDs, die seit v1.1.16 erzeugt werden, von Natur aus kryptographisch sicher sind.",
"Key to sign sessions with": "Schlüssel zum Anmelden von Sitzungen mit",
"Session Secret": "Sitzungsgeheimnis"
}, },
"Section": { "Section": {
"Insert section above": "Abschnitt oben einfügen", "Insert section above": "Abschnitt oben einfügen",

@ -1541,6 +1541,7 @@
"Error": "Error", "Error": "Error",
"Error checking for updates": "Error checking for updates", "Error checking for updates": "Error checking for updates",
"Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.": "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.", "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.": "Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.",
"Grist is up to date": "Grist is up to date", "Grist is up to date": "Grist is up to date",
"Grist releases are at ": "Grist releases are at ", "Grist releases are at ": "Grist releases are at ",
"Last checked {{time}}": "Last checked {{time}}", "Last checked {{time}}": "Last checked {{time}}",
@ -1567,7 +1568,10 @@
"Results": "Results", "Results": "Results",
"Self Checks": "Self Checks", "Self Checks": "Self Checks",
"You do not have access to the administrator panel.\nPlease log in as an administrator.": "You do not have access to the administrator panel.\nPlease log in as an administrator.", "You do not have access to the administrator panel.\nPlease log in as an administrator.": "You do not have access to the administrator panel.\nPlease log in as an administrator.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people." "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.",
"Key to sign sessions with": "Key to sign sessions with",
"Session Secret": "Session Secret"
}, },
"Columns": { "Columns": {
"Remove Column": "Remove Column" "Remove Column": "Remove Column"

@ -289,7 +289,9 @@
"Reload data engine": "Recargar el motor de datos", "Reload data engine": "Recargar el motor de datos",
"Reload data engine?": "¿Recargar motor de datos?", "Reload data engine?": "¿Recargar motor de datos?",
"You can make changes to the document, then stop timing to see the results.": "Puede realizar cambios en el documento y luego detener el cronometraje para ver los resultados.", "You can make changes to the document, then stop timing to see the results.": "Puede realizar cambios en el documento y luego detener el cronometraje para ver los resultados.",
"Stop timing...": "Dejando de cronometrar..." "Stop timing...": "Dejando de cronometrar...",
"Only available to document editors": "Sólo disponible para editores de documentos",
"Only available to document owners": "Solo disponible para los propietarios de documentos"
}, },
"DuplicateTable": { "DuplicateTable": {
"Copy all data in addition to the table structure.": "Copiar todos los datos además de la estructura de la tabla.", "Copy all data in addition to the table structure.": "Copiar todos los datos además de la estructura de la tabla.",
@ -1605,7 +1607,11 @@
"Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "O, como alternativa, puedes configurar: {{bootKey}} en el entorno y visita: {{url}}", "Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "O, como alternativa, puedes configurar: {{bootKey}} en el entorno y visita: {{url}}",
"You do not have access to the administrator panel.\nPlease log in as an administrator.": "No tienes acceso al panel de administrador.\nInicia sesión como administrador.", "You do not have access to the administrator panel.\nPlease log in as an administrator.": "No tienes acceso al panel de administrador.\nInicia sesión como administrador.",
"Self Checks": "Controles automáticos", "Self Checks": "Controles automáticos",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist permite configurar diferentes tipos de autenticación, incluidos SAML y OIDC. Recomendamos habilitar uno de estos si se puede acceder a Grist a través de la red o si está disponible para varias personas." "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist permite configurar diferentes tipos de autenticación, incluidos SAML y OIDC. Recomendamos habilitar uno de estos si se puede acceder a Grist a través de la red o si está disponible para varias personas.",
"Session Secret": "Secreto de sesión",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist firma las cookies de sesión de usuario con una clave secreta. Establezca esta clave mediante la variable de entorno GRIST_SESSION_SECRET. Si no se establece, Grist vuelve a un valor predeterminado. Es posible que quitemos este aviso en el futuro, ya que los identificadores de sesión generados desde la versión 1.1.16 son intrínsecamente seguros desde el punto de vista criptográfico.",
"Key to sign sessions with": "Clave para firmar sesiones con",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist firma las cookies de sesión de usuario con una clave secreta. Establezca esta clave mediante la variable de entorno GRIST_SESSION_SECRET. Si no se establece, Grist vuelve a un valor predeterminado. Es posible que quitemos este aviso en el futuro, ya que los identificadores de sesión generados desde la versión 1.1.16 son intrínsecamente seguros desde el punto de vista criptográfico."
}, },
"CreateTeamModal": { "CreateTeamModal": {
"Cancel": "Cancelar", "Cancel": "Cancelar",

@ -41,7 +41,8 @@
"Trash": "Cestino", "Trash": "Cestino",
"Workspace will be moved to Trash.": "Lo spazio di lavoro sarà spostato nel cestino.", "Workspace will be moved to Trash.": "Lo spazio di lavoro sarà spostato nel cestino.",
"Workspaces": "Spazi di lavoro", "Workspaces": "Spazi di lavoro",
"Tutorial": "Tutorial" "Tutorial": "Tutorial",
"Terms of service": "Condizioni di servizio"
}, },
"MakeCopyMenu": { "MakeCopyMenu": {
"However, it appears to be already identical.": "Tuttavia, sembra essere già identico.", "However, it appears to be already identical.": "Tuttavia, sembra essere già identico.",
@ -549,7 +550,9 @@
"Legacy": "Vecchia versione", "Legacy": "Vecchia versione",
"Personal Site": "Sito personale", "Personal Site": "Sito personale",
"Team Site": "Sito del team", "Team Site": "Sito del team",
"Grist Templates": "Template di Grist" "Grist Templates": "Template di Grist",
"Billing Account": "Dati di fatturazione",
"Manage Team": "Gestisci il team"
}, },
"ChartView": { "ChartView": {
"Create separate series for each value of the selected column.": "Creare serie separate per ciascun valore delle colonne selezionate.", "Create separate series for each value of the selected column.": "Creare serie separate per ciascun valore delle colonne selezionate.",
@ -726,7 +729,19 @@
"Currency": "Valuta", "Currency": "Valuta",
"Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}": "ID Documento da usare quando le Api REST chiedono {{docId}}. Vedi {{apiURL}}", "Document ID to use whenever the REST API calls for {{docId}}. See {{apiURL}}": "ID Documento da usare quando le Api REST chiedono {{docId}}. Vedi {{apiURL}}",
"Python version used": "Versione di Python in uso", "Python version used": "Versione di Python in uso",
"Try API calls from the browser": "Prova le chiamate Api nel browser" "Try API calls from the browser": "Prova le chiamate Api nel browser",
"Stop timing...": "Ferma cronometro...",
"Timing is on": "Cronometro attivo",
"You can make changes to the document, then stop timing to see the results.": "Puoi fare modifiche al documento, quindi fermare il cronometro e vedere il risultato.",
"Cancel": "Annulla",
"Reload data engine?": "Ricarica il motore dati?",
"Force reload the document while timing formulas, and show the result.": "Ricarica il documenti cronometrando le formule, e mostra il risultato.",
"Formula timer": "Cronometro per le formule",
"Reload data engine": "Ricarica il motore dati",
"Start timing": "Avvia cronometro",
"Time reload": "Ricarica tempo",
"Only available to document editors": "Disponibile solo per gli editor del documento",
"Only available to document owners": "Disponibile solo per i proprietari del documento"
}, },
"DocumentUsage": { "DocumentUsage": {
"Data Size": "Dimensione dei dati", "Data Size": "Dimensione dei dati",
@ -1106,7 +1121,9 @@
"Add conditional style": "Aggiungi stile condizionale", "Add conditional style": "Aggiungi stile condizionale",
"Error in style rule": "Errore nella regola di stile", "Error in style rule": "Errore nella regola di stile",
"Row Style": "Stile riga", "Row Style": "Stile riga",
"Rule must return True or False": "La regola deve restituire Vero o Falso" "Rule must return True or False": "La regola deve restituire Vero o Falso",
"Conditional Style": "Stile condizionale",
"IF...": "SE..."
}, },
"CurrencyPicker": { "CurrencyPicker": {
"Invalid currency": "Valuta non valida" "Invalid currency": "Valuta non valida"
@ -1466,7 +1483,22 @@
"Check now": "Controlla adesso", "Check now": "Controlla adesso",
"Checking for updates...": "Controllo gli aggiornamenti...", "Checking for updates...": "Controllo gli aggiornamenti...",
"Grist releases are at ": "Le release di Grist sono a ", "Grist releases are at ": "Le release di Grist sono a ",
"Sandbox settings for data engine": "Impostazione della sandbox per il motore dati" "Sandbox settings for data engine": "Impostazione della sandbox per il motore dati",
"Current authentication method": "Metodo di autenticazione attuale",
"No fault detected.": "Nessun problema rilevato.",
"Or, as a fallback, you can set: {{bootKey}} in the environment and visit: {{url}}": "O, come fallback, puoi impostare: {{bootKey}} nell'ambiente, e visitare: {{url}}",
"Results": "Risultati",
"Self Checks": "Auto-diagnostica",
"You do not have access to the administrator panel.\nPlease log in as an administrator.": "Non hai accesso al pannello di amministrazione.\nAccedi come amministratore.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone.",
"Details": "Dettagli",
"Notes": "Note",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "Grist firma i cookie della sessione con una chiave segreta. Impostare questa chiave con la variabile d'ambiente GRIST_SESSION_SECRET. Se questa non è definita, Grist usa un default non modificabile. Potremmo rimuovere questo avviso in futuro, perché gli ID di sessione generati a partire dalla versione 1.1.16 sono intrinsecamente sicuri dal punto di vista crittografico.",
"Administrator Panel Unavailable": "Pannello di amministrazione non disponibile",
"Authentication": "Autenticazione",
"Check failed.": "Controllo fallito.",
"Check succeeded.": "Controllo riuscito.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone."
}, },
"WelcomeCoachingCall": { "WelcomeCoachingCall": {
"Maybe Later": "Forse più tardi", "Maybe Later": "Forse più tardi",
@ -1587,5 +1619,15 @@
"Chart": "Grafico", "Chart": "Grafico",
"Custom": "Personalizzato", "Custom": "Personalizzato",
"Form": "Modulo" "Form": "Modulo"
},
"TimingPage": {
"Average Time (s)": "Media tempi (sec)",
"Total Time (s)": "Tempo totale (sec)",
"Loading timing data. Don't close this tab.": "Caricamento dei dati cronometrici. Non chiudere questa scheda.",
"Max Time (s)": "Tempo massimo (sec)",
"Number of Calls": "Numero di chiamate",
"Table ID": "ID Tabella",
"Column ID": "ID colonna",
"Formula timer": "Cronometro formule"
} }
} }

@ -347,7 +347,9 @@
"Reload data engine": "Recarregar o motor de dados", "Reload data engine": "Recarregar o motor de dados",
"Reload data engine?": "Recarregar o motor de dados?", "Reload data engine?": "Recarregar o motor de dados?",
"Start timing": "Iniciar cronometragem", "Start timing": "Iniciar cronometragem",
"Stop timing...": "Pare de cronometrar..." "Stop timing...": "Pare de cronometrar...",
"Only available to document editors": "Disponível apenas para editores de documentos",
"Only available to document owners": "Disponível apenas para proprietários de documentos"
}, },
"DocumentUsage": { "DocumentUsage": {
"Attachments Size": "Tamanho dos Anexos", "Attachments Size": "Tamanho dos Anexos",
@ -1615,7 +1617,11 @@
"Check failed.": "A verificação falhou.", "Check failed.": "A verificação falhou.",
"Current authentication method": "Método de autenticação atual", "Current authentication method": "Método de autenticação atual",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas.", "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas." "Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "O Grist permite a configuração de diferentes tipos de autenticação, incluindo SAML e OIDC. Recomendamos ativar um desses tipos se o Grist for acessível pela rede ou estiver sendo disponibilizado para várias pessoas.",
"Key to sign sessions with": "Chave para assinar sessões com",
"Session Secret": "Segredo da sessão",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future since session IDs have been updated to be inherently cryptographically secure.": "O Grist assina os cookies de sessão do usuário com uma chave secreta. Defina essa chave por meio da variável de ambiente GRIST_SESSION_SECRET. O Grist retorna a um padrão codificado quando ele não está definido. Poderemos remover esse aviso no futuro, pois os IDs de sessão gerados desde a versão 1.1.16 são inerentemente seguros em termos de criptografia.",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "O Grist assina os cookies de sessão do usuário com uma chave secreta. Defina essa chave por meio da variável de ambiente GRIST_SESSION_SECRET. O Grist retorna a um padrão codificado quando ele não está definido. Poderemos remover esse aviso no futuro, pois os IDs de sessão gerados desde a versão 1.1.16 são inerentemente seguros em termos de criptografia."
}, },
"Field": { "Field": {
"No choices configured": "Nenhuma opção configurada", "No choices configured": "Nenhuma opção configurada",

File diff suppressed because it is too large Load Diff

@ -538,7 +538,9 @@
"Cancel": "Prekliči", "Cancel": "Prekliči",
"Force reload the document while timing formulas, and show the result.": "Prisilno znova naloži dokument med časovnimi formulami in prikaži rezultat.", "Force reload the document while timing formulas, and show the result.": "Prisilno znova naloži dokument med časovnimi formulami in prikaži rezultat.",
"Timing is on": "Merjenje časa je vklopljeno", "Timing is on": "Merjenje časa je vklopljeno",
"You can make changes to the document, then stop timing to see the results.": "Dokument lahko spremeniš in nato ustaviš merjenje časa, da vidiš rezultat." "You can make changes to the document, then stop timing to see the results.": "Dokument lahko spremeniš in nato ustaviš merjenje časa, da vidiš rezultat.",
"Only available to document editors": "Na voljo samo urednikom dokumentov",
"Only available to document owners": "Na voljo samo lastnikom dokumentov"
}, },
"GridOptions": { "GridOptions": {
"Horizontal Gridlines": "Vodoravne linije", "Horizontal Gridlines": "Vodoravne linije",

@ -42,6 +42,8 @@ import {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/mi
import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit'; import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit';
import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares'; import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares';
import {Billing1711557445716 as BillingFeatures} from 'app/gen-server/migration/1711557445716-Billing'; import {Billing1711557445716 as BillingFeatures} from 'app/gen-server/migration/1711557445716-Billing';
import {UserLastConnection1713186031023
as UserLastConnection} from 'app/gen-server/migration/1713186031023-UserLastConnection';
const home: HomeDBManager = new HomeDBManager(); const home: HomeDBManager = new HomeDBManager();
@ -50,7 +52,8 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE
CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs, CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs,
ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart, ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart,
DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID, DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID,
Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures]; Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures,
UserLastConnection];
// Assert that the "members" acl rule and group exist (or not). // Assert that the "members" acl rule and group exist (or not).
function assertMembersGroup(org: Organization, exists: boolean) { function assertMembersGroup(org: Organization, exists: boolean) {
@ -113,6 +116,33 @@ describe('migrations', function() {
// be doing something. // be doing something.
}); });
it('can migrate UserUUID and UserUniqueRefUUID with user in table', async function() {
this.timeout(60000);
const runner = home.connection.createQueryRunner();
// Create 400 users to test the chunk (each chunk is 300 users)
const nbUsersToCreate = 400;
for (const migration of migrations) {
if (migration === UserUUID) {
for (let i = 0; i < nbUsersToCreate; i++) {
await runner.query(`INSERT INTO users (id, name, is_first_time_user) VALUES (${i}, 'name${i}', true)`);
}
}
await (new migration()).up(runner);
}
// Check that all refs are unique
const userList = await runner.manager.createQueryBuilder()
.select(["users.id", "users.ref"])
.from("users", "users")
.getMany();
const setOfUserRefs = new Set(userList.map(u => u.ref));
assert.equal(nbUsersToCreate, userList.length);
assert.equal(setOfUserRefs.size, userList.length);
await addSeedData(home.connection);
});
it('can correctly switch display_email column to non-null with data', async function() { it('can correctly switch display_email column to non-null with data', async function() {
this.timeout(60000); this.timeout(60000);
const sqlite = home.connection.driver.options.type === 'sqlite'; const sqlite = home.connection.driver.options.type === 'sqlite';

@ -3,6 +3,9 @@
"files": [], "files": [],
"include": [], "include": [],
"references": [ "references": [
{ "path": "./app" },
{ "path": "./stubs/app" },
{ "path": "./test" },
{ "path": "./ext/app" } { "path": "./ext/app" }
], ],
} }

Loading…
Cancel
Save