Skip to content

Commit

Permalink
feat(datasource sql): add option to see paranoid (#1210)
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaud-moncel authored Nov 20, 2024
1 parent 08e56b1 commit 2d2639c
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 5 deletions.
13 changes: 10 additions & 3 deletions packages/datasource-sql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { PlainConnectionOptions, PlainConnectionOptionsOrUri, SslMode } from './types';
import type {
PlainConnectionOptions,
PlainConnectionOptionsOrUri,
SqlDatasourceOptions,
SslMode,
} from './types';
import type { DataSourceFactory, Logger } from '@forestadmin/datasource-toolkit';

import { SequelizeDataSource } from '@forestadmin/datasource-sequelize';
Expand Down Expand Up @@ -39,14 +44,15 @@ async function buildModelsAndRelations(
sequelize: Sequelize,
logger: Logger,
introspection: SupportedIntrospection,
displaySoftDeleted?: SqlDatasourceOptions['displaySoftDeleted'],
): Promise<LatestIntrospection> {
try {
const latestIntrospection = await Introspector.migrateOrIntrospect(
sequelize,
logger,
introspection,
);
ModelBuilder.defineModels(sequelize, logger, latestIntrospection);
ModelBuilder.defineModels(sequelize, logger, latestIntrospection, displaySoftDeleted);
RelationBuilder.defineRelations(sequelize, logger, latestIntrospection);

return latestIntrospection;
Expand Down Expand Up @@ -84,14 +90,15 @@ export async function buildSequelizeInstance(

export function createSqlDataSource(
uriOrOptions: PlainConnectionOptionsOrUri,
options?: { introspection?: SupportedIntrospection },
options?: SqlDatasourceOptions,
): DataSourceFactory {
return async (logger: Logger) => {
const sequelize = await connect(new ConnectionOptions(uriOrOptions, logger));
const latestIntrospection = await buildModelsAndRelations(
sequelize,
logger,
options?.introspection,
options?.displaySoftDeleted,
);

return new SqlDatasource(new SequelizeDataSource(sequelize, logger), latestIntrospection.views);
Expand Down
16 changes: 15 additions & 1 deletion packages/datasource-sql/src/orm-builder/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Literal } from 'sequelize/types/utils';

import SequelizeTypeFactory from './helpers/sequelize-type';
import { LatestIntrospection, Table } from '../introspection/types';
import { SqlDatasourceOptions } from '../types';

type TableOrView = Table & { view?: boolean };

Expand All @@ -24,9 +25,15 @@ export default class ModelBuilder {
sequelize: Sequelize,
logger: Logger,
introspection: LatestIntrospection,
displaySoftDeleted?: SqlDatasourceOptions['displaySoftDeleted'],
): void {
for (const table of introspection.tables) {
this.defineModelFromTable(sequelize, logger, table);
const shouldDisplaySoftDeleted =
displaySoftDeleted === true ||
(Array.isArray(displaySoftDeleted) &&
displaySoftDeleted.some(tableName => table.name === tableName));

this.defineModelFromTable(sequelize, logger, table, shouldDisplaySoftDeleted);
}

for (const table of introspection.views) {
Expand All @@ -38,6 +45,7 @@ export default class ModelBuilder {
sequelize: Sequelize,
logger: Logger,
table: TableOrView,
displaySoftDeleted?: boolean,
): void {
const hasTimestamps = this.hasTimestamps(table);
const isParanoid = this.isParanoid(table);
Expand All @@ -53,6 +61,12 @@ export default class ModelBuilder {
...this.getAutoTimestampFieldsOverride(table),
});

if (displaySoftDeleted) {
model.beforeFind(option => {
option.paranoid = false;
});
}

// @see https://sequelize.org/docs/v6/other-topics/legacy/#primary-keys
// Tell sequelize NOT to invent primary keys when we don't provide them.
// (Note that this does not seem to work)
Expand Down
7 changes: 7 additions & 0 deletions packages/datasource-sql/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Options } from 'sequelize/types';
import { ConnectConfig } from 'ssh2';

import { SupportedIntrospection } from './introspection/types';

type SupportedSequelizeOptions = Pick<
Options,
| 'database'
Expand Down Expand Up @@ -44,3 +46,8 @@ export type PlainConnectionOptions = SupportedSequelizeOptions & {
export type PlainConnectionOptionsOrUri = PlainConnectionOptions | string;

export type SslMode = 'preferred' | 'disabled' | 'required' | 'verify' | 'manual';

export type SqlDatasourceOptions = {
introspection?: SupportedIntrospection;
displaySoftDeleted?: string[] | true;
};
44 changes: 44 additions & 0 deletions packages/datasource-sql/test/_helpers/setup-soft-deleted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DataTypes, Sequelize } from 'sequelize';

import { ConnectionDetails } from './connection-details';
import setupEmptyDatabase from './setup-empty-database';

export default async function setupSoftDeleted(
connectionDetails: ConnectionDetails,
database: string,
schema?: string,
): Promise<Sequelize> {
let sequelize: Sequelize | null = null;

try {
const optionalSchemaOption = schema ? { schema } : {};

sequelize = await setupEmptyDatabase(connectionDetails, database, optionalSchemaOption);

if (schema) {
await sequelize.getQueryInterface().dropSchema(schema);
await sequelize.getQueryInterface().createSchema(schema);
}

sequelize.define(
'softDeleted',
{ name: DataTypes.STRING },
{ tableName: 'softDeleted', ...optionalSchemaOption, timestamps: true, paranoid: true },
);

sequelize.define(
'softDeleted2',
{ name: DataTypes.STRING },
{ tableName: 'softDeleted2', ...optionalSchemaOption, timestamps: true, paranoid: true },
);

await sequelize.sync({ force: true, ...optionalSchemaOption });

return sequelize;
} catch (e) {
console.error('Error', e);
throw e;
} finally {
await sequelize?.close();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { SequelizeDataSource } from '@forestadmin/datasource-sequelize';
import { Projection } from '@forestadmin/datasource-toolkit';
import { caller, filter } from '@forestadmin/datasource-toolkit/dist/test/__factories__';
import { stringify } from 'querystring';
import { DataTypes, Dialect, Model, ModelStatic, Op, Sequelize } from 'sequelize';

import { buildSequelizeInstance, introspect } from '../../src';
import { buildSequelizeInstance, createSqlDataSource, introspect } from '../../src';
import Introspector from '../../src/introspection/introspector';
import { CONNECTION_DETAILS } from '../_helpers/connection-details';
import setupEmptyDatabase from '../_helpers/setup-empty-database';
import setupDatabaseWithIdNotPrimary from '../_helpers/setup-id-is-not-a-pk';
import setupSimpleTable from '../_helpers/setup-simple-table';
import setupSoftDeleted from '../_helpers/setup-soft-deleted';
import setupDatabaseWithTypes, { getAttributeMapping } from '../_helpers/setup-using-all-types';
import setupDatabaseWithRelations, { RELATION_MAPPING } from '../_helpers/setup-using-relations';

Expand Down Expand Up @@ -439,6 +443,86 @@ describe('SqlDataSourceFactory > Integration', () => {
expect(modelAssociations).toMatchObject(RELATION_MAPPING);
});
});

describe('with soft deleted record', () => {
const databaseName = 'datasource-sql-softdeleted-test';

describe('when display soft deleted only on one table', () => {
it('should only display records of that table', async () => {
const logger = jest.fn();
await setupSoftDeleted(connectionDetails, databaseName, schema);

const sqlDs = await createSqlDataSource(
`${connectionDetails.url(databaseName)}?${queryString}`,
{ displaySoftDeleted: ['softDeleted'] },
)(logger);

const collection = sqlDs.getCollection('softDeleted');
const collection2 = sqlDs.getCollection('softDeleted2');

await collection.create(caller.build(), [
{ name: 'shouldDisplay', deletedAt: Date.now() },
]);
await collection2.create(caller.build(), [
{ name: 'shouldNotDisplay', deletedAt: Date.now() },
]);

const records = await collection.list(
caller.build(),
filter.build(),
new Projection('name', 'deletedAt'),
);
const records2 = await collection2.list(
caller.build(),
filter.build(),
new Projection('name', 'deletedAt'),
);

await (sqlDs as SequelizeDataSource).close();

expect(records).toHaveLength(1);
expect(records2).toHaveLength(0);
});
});

describe('when display soft deleted for all tables', () => {
it('should display records on all tables', async () => {
const logger = jest.fn();
await setupSoftDeleted(connectionDetails, databaseName, schema);

const sqlDs = await createSqlDataSource(
`${connectionDetails.url(databaseName)}?${queryString}`,
{ displaySoftDeleted: true },
)(logger);

const collection = sqlDs.getCollection('softDeleted');
const collection2 = sqlDs.getCollection('softDeleted2');

await collection.create(caller.build(), [
{ name: 'shouldDisplay', deletedAt: Date.now() },
]);
await collection2.create(caller.build(), [
{ name: 'shouldNotDisplay', deletedAt: Date.now() },
]);

const records = await collection.list(
caller.build(),
filter.build(),
new Projection('name', 'deletedAt'),
);
const records2 = await collection2.list(
caller.build(),
filter.build(),
new Projection('name', 'deletedAt'),
);

await (sqlDs as SequelizeDataSource).close();

expect(records).toHaveLength(1);
expect(records2).toHaveLength(1);
});
});
});
});
});

Expand Down

0 comments on commit 2d2639c

Please sign in to comment.