Skip to content

Commit

Permalink
Add rule mandatory-columns (#345)
Browse files Browse the repository at this point in the history
* Add rule mandatory-columns

* Describe mandatory-columns
  • Loading branch information
orangain authored Feb 21, 2024
1 parent a122f30 commit 0c7d8fe
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,18 @@ rules: {
'row-level-security': ['error', {enforced: true}],
}
```

## mandatory-columns

This rule enforces that a table has certain columns. The option is an object, where the key is the column name and the value is the object representing the required properties. Any property of the [TableColumn](https://kristiandupont.github.io/extract-pg-schema/api/extract-pg-schema.tablecolumn.html) object can be used as a required property. For example, you can specify `ordinalPosition` to ensure that the column is in the expected position, but note that PostgreSQL always adds a new column to the very back.

```js
rules: {
'mandatory-columns': ['error', {
created_at: {
expandedType: 'pg_catalog.timestamptz',
isNullable: false,
}
}],
}
```
1 change: 1 addition & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./mandatoryColumns";
export * from "./nameCasing";
export * from "./nameInflection";
export * from "./references";
Expand Down
268 changes: 268 additions & 0 deletions src/rules/mandatoryColumns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { Schema } from "extract-pg-schema";
import { describe, expect, it, vi } from "vitest";

import DeepPartial from "../tests/DeepPartial";
import { mandatoryColumns } from "./mandatoryColumns";

describe("mandatoryColumns", () => {
describe("no tables", () => {
it("should pass when no tables exist", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
tables: [],
views: [],
};

mandatoryColumns.process({
options: [{}],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(0);
});
});

describe("single mandatory column", () => {
it("should pass when mandatory column exists", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [
{
name: "id",
expandedType: "pg_catalog.int4",
ordinalPosition: 1,
},
],
},
],
};

mandatoryColumns.process({
options: [{ id: { expandedType: "pg_catalog.int4" } }],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(0);
});

it("should report when mandatory column does not exist", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [],
},
],
};

mandatoryColumns.process({
options: [{ id: { expandedType: "pg_catalog.int4" } }],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(1);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test`,
message: `Mandatory column "id" is missing`,
}),
);
});

it("should report when mandatory column exists but type differs", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [
{
name: "id",
expandedType: "pg_catalog.int2",
ordinalPosition: 1,
},
],
},
],
};

mandatoryColumns.process({
options: [{ id: { expandedType: "pg_catalog.int4" } }],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(1);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test.id`,
message: `Column "id" has properties {"expandedType":"pg_catalog.int2"} but expected {"expandedType":"pg_catalog.int4"}`,
}),
);
});

it("should report when mandatory column exists but ordinalPosition differs", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [
{
name: "id",
expandedType: "pg_catalog.int2",
ordinalPosition: 1,
},
],
},
],
};

mandatoryColumns.process({
options: [{ id: { ordinalPosition: 2 } }],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(1);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test.id`,
message: `Column "id" has properties {"ordinalPosition":1} but expected {"ordinalPosition":2}`,
}),
);
});
});

describe("multiple mandatory columns", () => {
it("should pass when multiple mandatory columns exist", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [
{
name: "id",
expandedType: "pg_catalog.int4",
ordinalPosition: 1,
},
{
name: "created_at",
expandedType: "pg_catalog.timestamptz",
ordinalPosition: 2,
},
],
},
],
};

mandatoryColumns.process({
options: [
{
id: { expandedType: "pg_catalog.int4" },
created_at: { expandedType: "pg_catalog.timestamptz" },
},
],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(0);
});

it("should report when one of the multiple mandatory columns does not exist", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [
{
name: "id",
expandedType: "pg_catalog.int4",
ordinalPosition: 1,
},
],
},
],
};

mandatoryColumns.process({
options: [
{
id: { expandedType: "pg_catalog.int4" },
created_at: { expandedType: "pg_catalog.timestamptz" },
},
],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(1);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test`,
message: `Mandatory column "created_at" is missing`,
}),
);
});

it("should report when multiple mandatory columns do not exist", () => {
const mockReporter = vi.fn();
const schemaObject: DeepPartial<Schema> = {
name: "schema",
tables: [
{
name: "test",
columns: [],
},
],
};

mandatoryColumns.process({
options: [
{
id: { expandedType: "pg_catalog.int4" },
created_at: { expandedType: "pg_catalog.timestamptz" },
},
],
schemaObject: schemaObject as Schema,
report: mockReporter,
});

expect(mockReporter).toBeCalledTimes(2);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test`,
message: `Mandatory column "id" is missing`,
}),
);
expect(mockReporter).toBeCalledWith(
expect.objectContaining({
rule: "mandatory-columns",
identifier: `schema.test`,
message: `Mandatory column "created_at" is missing`,
}),
);
});
});
});
41 changes: 41 additions & 0 deletions src/rules/mandatoryColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { TableColumn, TableDetails } from "extract-pg-schema";
import * as R from "ramda";

import Rule from "../Rule";

export const mandatoryColumns: Rule = {
name: "mandatory-columns",
docs: {
description:
"Require tables to have certain columns with certain properties",
},
process({ options: [option], schemaObject, report }) {
const expectedColumns: Record<string, Partial<TableColumn>> = option ?? {};
const validator = ({ name: tableName, columns }: TableDetails) => {
const columnsByName = R.indexBy(R.prop("name"), columns);
Object.entries(expectedColumns).forEach(([name, expectedProps]) => {
const column = columnsByName[name];
if (!column) {
report({
rule: this.name,
identifier: `${schemaObject.name}.${tableName}`,
message: `Mandatory column "${name}" is missing`,
});
} else {
const partialColumnProps = R.pick(
Object.keys(expectedProps) as (keyof TableColumn)[],
column,
);
if (!R.equals(partialColumnProps, expectedProps)) {
report({
rule: this.name,
identifier: `${schemaObject.name}.${tableName}.${column.name}`,
message: `Column "${column.name}" has properties ${JSON.stringify(partialColumnProps)} but expected ${JSON.stringify(expectedProps)}`,
});
}
}
});
};
schemaObject.tables.forEach(validator);
},
};

0 comments on commit 0c7d8fe

Please sign in to comment.