Skip to content

Commit

Permalink
Merge pull request #59 from radify/develop
Browse files Browse the repository at this point in the history
Next release
  • Loading branch information
wms authored Mar 22, 2017
2 parents 99c17c4 + c4b909d commit 5d68669
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 16 deletions.
5 changes: 3 additions & 2 deletions integration-test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ var expected = {
description: {
type: 'string'
},
complete: {
type: 'boolean'
status: {
type: 'string',
enum: ['new', 'started', 'complete']
},
created_at: {
type: 'string',
Expand Down
12 changes: 9 additions & 3 deletions integration-test/setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ CREATE TABLE tasks
id serial,
title character varying(255) NOT NULL,
description character varying(255),
complete boolean NOT NULL DEFAULT false,
status text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
owner integer NOT NULL,
PRIMARY KEY (id)
PRIMARY KEY (id),

CONSTRAINT status_is_valid CHECK (
status = any(
array['new', 'started', 'complete']
)
)
);

CREATE TABLE users
Expand Down Expand Up @@ -39,7 +45,7 @@ FROM tasks
INNER JOIN users
ON users.id = tasks.owner
WHERE
tasks.complete = true
tasks.status = 'complete'
GROUP BY
users.id;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"dependencies": {
"bluebird": "^3.3.4",
"callsite": "^1.0.0",
"ramda": "^0.19.1"
"ramda": "^0.22.1"
},
"scripts": {
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
Expand Down
20 changes: 20 additions & 0 deletions spec/inspectors/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,26 @@ describe('schema', () => {
}
});
});

it('declares columns that use a CHECK constraint to match an array of values as `enum`', () => {
var result = property({
name: 'status',
nullable: false,
default: null,
type: 'text',
isprimarykey: false,
constraints: [
"(status = ANY (ARRAY['new'::text, 'started'::text, 'complete'::text]))"
]
});

expect(result).toEqual({
status: {
type: 'string',
enum: ['new', 'started', 'complete']
}
});
});
});

describe('.required()', () => {
Expand Down
12 changes: 11 additions & 1 deletion src/inspectors/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ SELECT
columns.is_nullable::boolean AS nullable,
columns.character_maximum_length AS length,
attributes.attndims AS dimensions,
indexes.indisprimary AS isPrimaryKey
indexes.indisprimary AS isPrimaryKey,
constraints

FROM information_schema.columns AS columns

Expand All @@ -19,6 +20,15 @@ LEFT JOIN pg_catalog.pg_index AS indexes
ON indexes.indrelid = attributes.attrelid
AND attributes.attnum = ANY(indexes.indkey)

LEFT JOIN (
SELECT conrelid, conkey, array_agg(consrc) AS constraints
FROM pg_catalog.pg_constraint
WHERE contype = 'c'
GROUP BY conrelid, conkey
) AS constraints
ON conrelid = attributes.attrelid
AND attributes.attnum = ANY(conkey)

WHERE columns.table_schema = 'public'
AND columns.table_name = ${name}

Expand Down
79 changes: 70 additions & 9 deletions src/inspectors/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Promise from 'bluebird';
import {IDatabase} from 'pg-promise';
import {mergeAll} from 'ramda';
import {mergeAll, flatten} from 'ramda';

import query from '../util/fileQuery';

Expand All @@ -13,6 +13,7 @@ export interface Column {
nullable: boolean;
default: string | number;
isprimarykey: boolean;
constraints?: string[];
}

/**
Expand All @@ -30,9 +31,17 @@ export interface SchemaDocument {
* Describes an object's properties according to the JSON Schema standard
*/
export interface SchemaProperties {
[name: string]: {
type: string
}
[name: string]: PropertyAttributes;
}

/**
* Describes an individual properties' attributes according to the JSON Schema
* standard
*/
export interface PropertyAttributes {
type: string;
readOnly?: boolean;
enum?: any[];
}

/**
Expand Down Expand Up @@ -126,20 +135,72 @@ export function property(column: Column): SchemaProperties {
'timestamp with time zone': {type: 'string', format: 'date-time'}
};

var col = TYPES[column.type];
return {
[column.name]: mergeAll<PropertyAttributes>([
TYPES[column.type],
isReadOnly(column),
isEnumConstraint(column)
])
};
}

/**
* Determines if a column should be declared read-only by determining if the
* column is a primary key that uses a sequence
*
* @param column The column to inspect
* @returns `{readOnly: true}` if the column should be marked read-only
*/
export function isReadOnly(column: Column) {
var isPrimary = column.isprimarykey &&
column.nullable === false &&
typeof column.default === 'string' &&
(column.default as string).startsWith('nextval');

if (isPrimary) {
col.readOnly = true;
return {readOnly: true};
}
}

return {
[column.name]: col
};
/**
* Determines if a column should be declared an enumeration by determining if
* the column contains a suitable CHECK constraint
*
* @param column the column to inspect
* @returns `{enum: [values]}` if the column contains a suitable CHECK contraint
*/
export function isEnumConstraint(column: Column) {
if (!column.constraints || !column.constraints.length) {
return;
}

const ENUM_CHECK_REGEX = /^\(.* = ANY \(ARRAY\[(.*)\]\)\)/;
const ENUM_EXTRACT_VALUE_REGEX = /^'(.*)'::.*/;

var values = column.constraints
.map(constraint => {
if (!constraint) {
return;
}

var [match, values] = constraint.match(ENUM_CHECK_REGEX);
if (!values) {
return;
}

return values.split(', ')
.map(value => {
var [match, value] = value.match(ENUM_EXTRACT_VALUE_REGEX);
return value;
})
})
.filter(values => !!values);

if (!values.length) {
return;
}

return {enum: flatten(values)};
}

/**
Expand Down

0 comments on commit 5d68669

Please sign in to comment.