mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
5ef889addd
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
226 lines
7.3 KiB
TypeScript
226 lines
7.3 KiB
TypeScript
import {MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey} from 'typeorm';
|
|
import {BillingAccount} from 'app/gen-server/entity/BillingAccount';
|
|
import {BillingAccountManager} from 'app/gen-server/entity/BillingAccountManager';
|
|
import {Organization} from 'app/gen-server/entity/Organization';
|
|
import {Product} from 'app/gen-server/entity/Product';
|
|
import {nativeValues} from 'app/gen-server/lib/values';
|
|
|
|
export class Billing1556726945436 implements MigrationInterface {
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
|
// Create table for products.
|
|
await queryRunner.createTable(new Table({
|
|
name: 'products',
|
|
columns: [
|
|
{
|
|
name: 'id',
|
|
type: 'integer',
|
|
isGenerated: true,
|
|
generationStrategy: 'increment',
|
|
isPrimary: true
|
|
},
|
|
{
|
|
name: 'name',
|
|
type: 'varchar'
|
|
},
|
|
{
|
|
name: 'stripe_product_id',
|
|
type: 'varchar',
|
|
isUnique: true,
|
|
isNullable: true
|
|
},
|
|
{
|
|
name: 'features',
|
|
type: nativeValues.jsonType
|
|
}
|
|
]
|
|
}));
|
|
|
|
// Create a basic free product that existing orgs can use.
|
|
const product = new Product();
|
|
product.name = 'Free';
|
|
product.features = {};
|
|
await queryRunner.manager.save(product);
|
|
|
|
// Create billing accounts and billing account managers.
|
|
await queryRunner.createTable(new Table({
|
|
name: 'billing_accounts',
|
|
columns: [
|
|
{
|
|
name: 'id',
|
|
type: 'integer',
|
|
isGenerated: true,
|
|
generationStrategy: 'increment',
|
|
isPrimary: true
|
|
},
|
|
{
|
|
name: 'product_id',
|
|
type: 'integer'
|
|
},
|
|
{
|
|
name: 'individual',
|
|
type: nativeValues.booleanType
|
|
},
|
|
{
|
|
name: 'in_good_standing',
|
|
type: nativeValues.booleanType,
|
|
default: nativeValues.trueValue
|
|
},
|
|
{
|
|
name: 'status',
|
|
type: nativeValues.jsonType,
|
|
isNullable: true
|
|
},
|
|
{
|
|
name: 'stripe_customer_id',
|
|
type: 'varchar',
|
|
isUnique: true,
|
|
isNullable: true
|
|
},
|
|
{
|
|
name: 'stripe_subscription_id',
|
|
type: 'varchar',
|
|
isUnique: true,
|
|
isNullable: true
|
|
},
|
|
{
|
|
name: 'stripe_plan_id',
|
|
type: 'varchar',
|
|
isNullable: true
|
|
}
|
|
],
|
|
foreignKeys: [
|
|
{
|
|
columnNames: ['product_id'],
|
|
referencedColumnNames: ['id'],
|
|
referencedTableName: 'products'
|
|
}
|
|
]
|
|
}));
|
|
|
|
await queryRunner.createTable(new Table({
|
|
name: 'billing_account_managers',
|
|
columns: [
|
|
{
|
|
name: 'id',
|
|
type: 'integer',
|
|
isGenerated: true,
|
|
generationStrategy: 'increment',
|
|
isPrimary: true
|
|
},
|
|
{
|
|
name: 'billing_account_id',
|
|
type: 'integer'
|
|
},
|
|
{
|
|
name: 'user_id',
|
|
type: 'integer'
|
|
}
|
|
],
|
|
foreignKeys: [
|
|
{
|
|
columnNames: ['billing_account_id'],
|
|
referencedColumnNames: ['id'],
|
|
referencedTableName: 'billing_accounts',
|
|
onDelete: 'CASCADE' // delete manager if referenced billing_account goes away
|
|
},
|
|
{
|
|
columnNames: ['user_id'],
|
|
referencedColumnNames: ['id'],
|
|
referencedTableName: 'users',
|
|
onDelete: 'CASCADE' // delete manager if referenced user goes away
|
|
}
|
|
]
|
|
}));
|
|
|
|
// Add a reference to billing accounts from orgs.
|
|
await queryRunner.addColumn('orgs', new TableColumn({
|
|
name: 'billing_account_id',
|
|
type: 'integer',
|
|
isNullable: true
|
|
}));
|
|
await queryRunner.createForeignKey('orgs', new TableForeignKey({
|
|
columnNames: ['billing_account_id'],
|
|
referencedColumnNames: ['id'],
|
|
referencedTableName: 'billing_accounts'
|
|
}));
|
|
|
|
// Let's add billing accounts to all existing orgs.
|
|
// Personal orgs are put on an individual billing account.
|
|
// Other orgs are put on a team billing account, with the
|
|
// list of payment managers seeded by owners of that account.
|
|
const query =
|
|
queryRunner.manager.createQueryBuilder()
|
|
.select('orgs.id')
|
|
.from(Organization, 'orgs')
|
|
.leftJoin('orgs.owner', 'owners')
|
|
.addSelect('orgs.owner.id')
|
|
.leftJoinAndSelect('orgs.aclRules', 'acl_rules')
|
|
.leftJoinAndSelect('acl_rules.group', 'groups')
|
|
.leftJoin('groups.memberUsers', 'users')
|
|
.addSelect('users.id')
|
|
.where('permissions & 8 = 8'); // seed managers with owners+editors, omitting guests+viewers
|
|
// (permission 8 is "Remove")
|
|
const orgs = await query.getMany();
|
|
for (const org of orgs) {
|
|
const individual = Boolean(org.owner);
|
|
const billingAccountInsert = await queryRunner.manager.createQueryBuilder()
|
|
.insert()
|
|
.into(BillingAccount)
|
|
.values([{product, individual}])
|
|
.execute();
|
|
const billingAccountId = billingAccountInsert.identifiers[0].id;
|
|
if (individual) {
|
|
await queryRunner.manager.createQueryBuilder()
|
|
.insert()
|
|
.into(BillingAccountManager)
|
|
.values([{billingAccountId, userId: org.owner.id}])
|
|
.execute();
|
|
} else {
|
|
for (const rule of org.aclRules) {
|
|
for (const user of rule.group.memberUsers) {
|
|
await queryRunner.manager.createQueryBuilder()
|
|
.insert()
|
|
.into(BillingAccountManager)
|
|
.values([{billingAccountId, userId: user.id}])
|
|
.execute();
|
|
}
|
|
}
|
|
}
|
|
await queryRunner.manager.createQueryBuilder()
|
|
.update(Organization)
|
|
.set({billingAccountId})
|
|
.where('id = :id', {id: org.id})
|
|
.execute();
|
|
}
|
|
|
|
// TODO: in a future migration, orgs.billing_account_id could be constrained
|
|
// to be non-null. All code deployments linked to a database that will be
|
|
// migrated must have code that sets orgs.billing_account_id by that time,
|
|
// otherwise they would fail to create orgs (and remember creating a user
|
|
// involves creating an org).
|
|
/*
|
|
// Now that all orgs have a billing account (and this migration is running within
|
|
// a transaction), we can constrain orgs.billing_account_id to be non-null.
|
|
const orgTable = (await queryRunner.getTable('orgs'))!;
|
|
const billingAccountId = orgTable.findColumnByName('billing_account_id')!;
|
|
const billingAccountIdNonNull = billingAccountId.clone();
|
|
billingAccountIdNonNull.isNullable = false;
|
|
await queryRunner.changeColumn('orgs', billingAccountId, billingAccountIdNonNull);
|
|
*/
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
|
// this is a bit ugly, but is the documented way to remove a foreign key
|
|
const table = await queryRunner.getTable('orgs');
|
|
const foreignKey = table!.foreignKeys.find(fk => fk.columnNames.indexOf('billing_account_id') !== -1);
|
|
await queryRunner.dropForeignKey('orgs', foreignKey!);
|
|
|
|
await queryRunner.dropColumn('orgs', 'billing_account_id');
|
|
await queryRunner.dropTable('billing_account_managers');
|
|
await queryRunner.dropTable('billing_accounts');
|
|
await queryRunner.dropTable('products');
|
|
}
|
|
|
|
}
|