Skip to content

feat(json-api-nestjs): Uuid support for relationships #41

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

Merged
merged 11 commits into from
Apr 3, 2023
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
28 changes: 28 additions & 0 deletions libs/json-api-nestjs/src/lib/factory/ajv/ajv.factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,34 @@ describe('AJV factory', () => {
required: ['type', 'id'],
});

expect(
dataProperty['relationships']['properties']['notes']['properties'][
'data'
]['type']
).toBe('array');
expect(
dataProperty['relationships']['properties']['notes']['properties'][
'data'
]['items']
).toEqual({
type: 'object',
properties: {
id: {
type: 'string',
description: 'Use string should be as uuid string',
pattern:
'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
maxLength: 36,
minLength: 36,
},
type: {
type: 'string',
enum: ['notes'],
},
},
required: ['type', 'id'],
});

expect(Object.keys(dataProperty['relationships']['properties'])).toEqual(
relationField
);
Expand Down
15 changes: 15 additions & 0 deletions libs/json-api-nestjs/src/lib/factory/ajv/ajv.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ export function AjvCallFactory(

for (const entity of options.entities) {
const arrayProps: { [key: string]: boolean } = {};
const uuidProps: { [key: string]: boolean } = {};
const relationArrayProps: { [key: string]: { [key: string]: boolean } } =
{};
const relationUuids: { [key: string]: { [key: string]: boolean } } = {};
const repository = dataSource.getRepository(entity);
const relations = repository.metadata.relations.map((i) => {
return i.propertyName;
Expand All @@ -36,6 +38,7 @@ export function AjvCallFactory(
.filter((i) => !relations.includes(i.propertyName))
.map((i) => {
arrayProps[i.propertyName] = i.isArray;
uuidProps[i.propertyName] = i.generationStrategy === 'uuid';
return i.propertyName;
});
const relationType = repository.metadata.relations.reduce((acum, i) => {
Expand Down Expand Up @@ -63,6 +66,15 @@ export function AjvCallFactory(
relationArrayProps[item.propertyName] =
relationArrayProps[item.propertyName] || {};
relationArrayProps[item.propertyName][i.propertyName] = i.isArray;

relationUuids[item.propertyName] =
relationUuids[item.propertyName] || {};

if (i.isPrimary) {
relationUuids[item.propertyName][i.propertyName] =
i.generationStrategy === 'uuid';
}

return i.propertyName;
});
const fakeObject = columns.reduce<Record<string, string>>(
Expand Down Expand Up @@ -110,6 +122,7 @@ export function AjvCallFactory(
arrayProps,
relationArrayProps,
relationType,
relationUuids,
}
),
`inputBodyPostSchema-${schemaName}`
Expand All @@ -122,8 +135,10 @@ export function AjvCallFactory(
`inputBodyPatchSchema-${schemaName}`,
{
arrayProps,
uuidProps,
relationArrayProps,
relationType,
relationUuids,
}
),
`inputBodyPatchSchema-${schemaName}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ export function inputBodyPatchSchema(
schemaName: string,
arrayPropsConfig: {
arrayProps: { [key: string]: boolean };
uuidProps: { [key: string]: boolean };
relationArrayProps: { [key: string]: { [key: string]: boolean } };
relationType: {
[key: string]: Function | string;
};
relationUuids: { [key: string]: { [key: string]: boolean } };
}
): ReturnType<typeof inputBodyPostSchema> {
const json = inputBodyPostSchema(
Expand All @@ -21,13 +23,18 @@ export function inputBodyPatchSchema(
schemaName,
arrayPropsConfig
);
const patternObject: Record<string, string> = {};
if (
Reflect.getMetadata('design:type', entity['prototype'], 'id') === Number
) {
patternObject.pattern = '^\\d+$';
patternObject.description = 'Use string should be as number string';
const patternObject: Record<string, string | number> = {};
patternObject.pattern = '^\\d+$';
patternObject.description = 'Use string should be as number string';

if (arrayPropsConfig.uuidProps.id) {
patternObject.pattern =
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$';
patternObject.maxLength = 36;
patternObject.minLength = 36;
patternObject.description = 'Use string should be as uuid string';
}

json.properties.data.properties = {
...{
id: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function inputBodyPostSchema(
relationType: {
[key: string]: Function | string;
};
relationUuids: { [key: string]: { [key: string]: boolean } };
}
): typeof inputBodySchemaJson {
const json: typeof inputBodySchemaJson = JSON.parse(
Expand All @@ -23,7 +24,7 @@ export function inputBodyPostSchema(
camelToKebab(getEntityName(entity))
);

const relDataType = {
const baseRelDataType: Record<string, any> = {
type: 'object',
properties: {
data: {
Expand Down Expand Up @@ -103,13 +104,44 @@ export function inputBodyPostSchema(
...attributes,
};

const uuidRelations = arrayPropsConfig.relationUuids || {};
const relationships = Object.keys(relationsField).reduce((acum, item) => {
const relDataType = {
[item]: {
...baseRelDataType,
properties: {
...baseRelDataType.properties,
data: {
...baseRelDataType.properties.data,
properties: {
...baseRelDataType.properties.data.properties,
id: {
...baseRelDataType.properties.data.properties.id,
pattern: uuidRelations[item]?.id
? '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
: '^\\d+$',
...(uuidRelations[item]?.id
? {
minLength: 36,
maxLength: 36,
}
: {}),
description: uuidRelations[item]?.id
? 'Use string should be as uuid string'
: 'Use string should be as number string',
},
},
},
},
},
};

const resultSchema = {
...relDataType.properties.data,
...relDataType[item].properties.data,
properties: {
...relDataType.properties.data.properties,
...relDataType[item].properties.data.properties,
type: {
...relDataType.properties.data.properties.type,
...relDataType[item].properties.data.properties.type,
...(arrayPropsConfig.relationType[item]
? {
enum: [
Expand All @@ -124,9 +156,9 @@ export function inputBodyPostSchema(
};

acum[item] = {
...relDataType,
...relDataType[item],
properties: {
...relDataType.properties,
...relDataType[item].properties,
data:
Reflect.getMetadata('design:type', entity['prototype'], item) ===
Array
Expand All @@ -138,6 +170,7 @@ export function inputBodyPostSchema(
: resultSchema,
},
};

return acum;
}, {});

Expand Down
37 changes: 3 additions & 34 deletions libs/json-api-nestjs/src/lib/mock-utils/db-for-test
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

create extension "uuid-ossp";

--
-- Name: comment_kind_enum; Type: TYPE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -103,33 +105,14 @@ ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id;
--

CREATE TABLE public.notes (
id integer NOT NULL,
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
text text NOT NULL,
created_by integer,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
);


--
-- Name: notes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE public.notes_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


--
-- Name: notes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE public.notes_id_seq OWNED BY public.notes.id;


--
-- Name: migrations; Type: TABLE; Schema: public; Owner: -
Expand Down Expand Up @@ -421,13 +404,6 @@ ALTER TABLE ONLY public.addresses ALTER COLUMN id SET DEFAULT nextval('public.ad
ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass);


--
-- Name: notes id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.notes ALTER COLUMN id SET DEFAULT nextval('public.notes_id_seq'::regclass);


--
-- Name: migrations id; Type: DEFAULT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -508,13 +484,6 @@ ALTER TABLE ONLY public.comments
ADD CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY (id);


--
-- Name: notes PK_notes; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.notes
ADD CONSTRAINT "PK_notes" PRIMARY KEY (id);

--
-- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: -
--
Expand Down
4 changes: 2 additions & 2 deletions libs/json-api-nestjs/src/lib/mock-utils/entities/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { Users, IUsers } from '.';

@Entity('notes')
export class Notes {
@PrimaryGeneratedColumn()
public id: number;
@PrimaryGeneratedColumn('uuid')
public id: string;

@IsNotEmpty()
@Column({
Expand Down
13 changes: 12 additions & 1 deletion libs/json-api-nestjs/src/lib/mock-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TypeOrmModule } from '@nestjs/typeorm';
import { DynamicModule } from '@nestjs/common';
import { newDb } from 'pg-mem';
import { DataType, newDb } from 'pg-mem';
import { readFileSync } from 'fs';
import { join } from 'path';

Expand All @@ -17,6 +17,8 @@ import {
} from './entities';
import { DataSource } from 'typeorm';

import { v4 } from 'uuid';

export * from './entities';

export const entities = [
Expand Down Expand Up @@ -48,6 +50,15 @@ export function mockDBTestModule(): DynamicModule {
'PostgreSQL 12.5 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, 64-bit',
});

db.registerExtension('uuid-ossp', (schema) => {
schema.registerFunction({
name: 'uuid_generate_v4',
returns: DataType.uuid,
implementation: v4,
impure: true,
});
});

db.public.none(dump);
const backup = db.backup();
return TypeOrmModule.forRootAsync({
Expand Down
Loading