Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datasource sql): add option to see paranoid #1210

Merged
merged 9 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminds me of this

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
Loading