Skip to content

Commit

Permalink
Change park_calendar to season
Browse files Browse the repository at this point in the history
  • Loading branch information
cressie176 committed Dec 5, 2024
1 parent 5b05ee3 commit 0471539
Show file tree
Hide file tree
Showing 35 changed files with 204 additions and 303 deletions.
88 changes: 34 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Filby has the following important concepts
A *Projection* is a versioned view of one or more *Entities*, made available in the example application via a RESTful API. The implementor is responsible for writing the the projections, which will mostly be simple database to JSON transformations.

### Entity
An *Entity* represents reference data items. Entities may be stand alone, or form an object graph. We use a holiday park as an example, in which a 'Park' entity has many 'Calendar Event' entities. If you are familiar with event sourcing, they are implemented as an aggregate of one or more *Data Frames*. The dependency between projections and entities must be explicitly stated so we can emit *Notifications* when a new data frame is added.
An *Entity* represents reference data. Entities may be stand alone, or form an object graph. We use a holiday park as an example, in which a 'Park' entity has many 'Season' entities. If you are familiar with event sourcing, they are implemented as an aggregate of one or more *Data Frames*. The dependency between projections and entities must be explicitly stated so we can emit *Notifications* when a new data frame is added.

### Data Frame
A *Data Frame* is a snapshot of an entity, associated with a *Change Set*. There are two types of data frame, 'POST' which adds a new snapshot at a point in time, and 'DELETE' which indicates the entity has been deleted.
Expand All @@ -221,12 +221,10 @@ All of above objects (Projections, Entities, Data Frames, etc) are defined using
# Add enums for your reference data
# Equivalent of PostgreSQL's CREATE TYPE statement
- operation: ADD_ENUM
name: park_calendar_event_type
name: season_type
values:
- Park Open - Owners
- Park Open - Guests
- Park Close - Owners
- Park Close - Guests
- Owners
- Guests

# Adding entities performs the following:
# 1. Inserts a row into the 'fby_entity' table,
Expand Down Expand Up @@ -256,7 +254,7 @@ All of above objects (Projections, Entities, Data Frames, etc) are defined using
dependencies:
- name: park
version: 1
- name: calendar_event
- name: season
version: 1

# A hook defines an asynchronous event that will be emitted by the framework whenever the
Expand Down Expand Up @@ -323,7 +321,7 @@ All of above objects (Projections, Entities, Data Frames, etc) are defined using
# Deletes the specified enum
# Fails if the enums are still use
- operation: DROP_ENUM
name: park_calendar_event_type
name: season_type

# Deletes the specified entity
# Fails if the entities are depended on by projections or used in change sets
Expand Down Expand Up @@ -366,14 +364,14 @@ In addition to deleting change sets, this can be useful for adding custom views
```sql
-- migrations/0005.create-get-park-v1-function.sql
CREATE FUNCTION get_park_v1(p_change_set_id INTEGER)
RETURNS TABLE (code TEXT, name TEXT, calendar_event park_calendar_event_type, calendar_occurs TIMESTAMP WITH TIME ZONE)
RETURNS TABLE (code TEXT, name TEXT, season_type season_type, season_start TIMESTAMPTZ, season_end TIMESTAMPTZ)
AS $$
BEGIN
RETURN QUERY
SELECT p.code, p.name, pc.event AS calendar_event, pc.occurs AS calendar_occurs
SELECT p.code, p.name, s.type AS season_type, s.start AS season_start, s.end AS season_end
FROM get_park_v1_aggregate(p_change_set_id) p
LEFT JOIN get_park_calendar_v1_aggregate(p_change_set_id) pc ON pc.park_code = p.code
ORDER BY p.code ASC, p.occurs ASC;
LEFT JOIN get_season_v1_aggregate(p_change_set_id) s ON s.park_code = p.code
ORDER BY p.code ASC, s.start DESC, s.type ASC;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
```
Expand Down Expand Up @@ -454,10 +452,10 @@ Passes a transactional [node-pg client](https://node-postgres.com/) to the given
async function getParks(changeSetId) {
return filby.withTransaction(async (client) => {
const { rows } = await client.query(`
SELECT p.code, p.name, pc.event AS calendar_event, pc.occurs AS calendar_occurs
SELECT p.code, p.name, s.type AS season_type, s.start AS season_start, s.end AS season_end
FROM get_park_v1_aggregate($1) p
LEFT JOIN get_park_calendar_v1_aggregate($1) pc ON pc.park_code = p.code
ORDER BY p.code ASC, pc.occurs ASC;
LEFT JOIN get_season_v1_aggregate($1) s ON s.park_code = p.code
ORDER BY p.code ASC, s.start DESC, s.type desc ASC;
`, [changeSetId]);
return rows.map(toParkStructure);
});
Expand Down Expand Up @@ -649,66 +647,48 @@ GET http://localhost:3000/api/projection/v1/park?changeSetId=1
{
"code":"DC",
"name":"Devon Cliffs",
"calendar":[
"seasons":[
{
"event":"Park Open - Owners",
"occurs":"2019-03-01T00:00:00.000Z"
"type":"Owners",
"start":"2019-03-01T00:00:00.000Z",
"end":"2019-11-30T00:00:00.000Z"
},
{
"event":"Park Open - Guests",
"occurs":"2019-03-15T00:00:00.000Z"
},
{
"event":"Park Close - Guests",
"occurs":"2019-11-15T00:00:00.000Z"
},
{
"event":"Park Close - Owners",
"occurs":"2019-11-30T00:00:00.000Z"
"type":"Guests",
"start":"2019-03-15T00:00:00.000Z",
"end":"2019-11-15T00:00:00.000Z"
}
]
},
{
"code":"GA",
"name":"Greenacres",
"calendar":[
{
"event":"Park Open - Owners",
"occurs":"2019-03-01T00:00:00.000Z"
},
"seasons":[
{
"event":"Park Open - Guests",
"occurs":"2019-03-15T00:00:00.000Z"
"type":"Owners",
"start":"2019-03-01T00:00:00.000Z",
"end":"2019-11-30T00:00:00.000Z"
},
{
"event":"Park Close - Guests",
"occurs":"2019-11-15T00:00:00.000Z"
},
{
"event":"Park Close - Owners",
"occurs":"2019-11-30T00:00:00.000Z"
"type":"Guests",
"start":"2019-03-15T00:00:00.000Z",
"end":"2019-11-15T00:00:00.000Z"
}
]
},
{
"code":"PV",
"name":"Primrose Valley",
"calendar":[
{
"event":"Park Open - Owners",
"occurs":"2019-03-01T00:00:00.000Z"
},
{
"event":"Park Open - Guests",
"occurs":"2019-03-15T00:00:00.000Z"
},
"seasons":[
{
"event":"Park Close - Guests",
"occurs":"2019-11-15T00:00:00.000Z"
"type":"Owners",
"start":"2019-03-01T00:00:00.000Z",
"end":"2019-11-30T00:00:00.000Z"
},
{
"event":"Park Close - Owners",
"occurs":"2019-11-30T00:00:00.000Z"
"type":"Guests",
"start":"2019-03-15T00:00:00.000Z",
"end":"2019-11-15T00:00:00.000Z"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions examples/javascript/lib/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module.exports = class Application {
async #registerWebhook(event, url) {
this.#filby.subscribe(event, async (notification) => {
const api = this.#projectionsApi.get(notification.projection);
// console.log(`POST ${url}`, { ...notification })
await axios.post(url, { ...notification, api });
});
}
Expand Down
10 changes: 5 additions & 5 deletions examples/javascript/lib/routes/park-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,24 @@ module.exports = (fastify, { projection, filby }, done) => {

async function getParks(changeSet) {
return filby.withTransaction(async (tx) => {
const { rows } = await tx.query('SELECT code, name, calendar_event, calendar_occurs FROM get_park_v1($1)', [changeSet.id]);
const { rows } = await tx.query('SELECT code, name, season_type, season_start, season_end FROM get_park_v1($1)', [changeSet.id]);
const parkDictionary = rows.reduce(toParkDictionary, new Map());
return Array.from(parkDictionary.values());
});
}

async function getPark(changeSet, code) {
return filby.withTransaction(async (tx) => {
const { rows } = await tx.query('SELECT code, name, calendar_event, calendar_occurs FROM get_park_v1($1) WHERE code = upper($2)', [changeSet.id, code]);
const { rows } = await tx.query('SELECT code, name, season_type, season_start, season_end FROM get_park_v1($1) WHERE code = upper($2)', [changeSet.id, code]);
const parkDictionary = rows.reduce(toParkDictionary, new Map());
return parkDictionary.get(code);
});
}

function toParkDictionary(dictionary, row) {
const { code, name, calendar_event: event, calendar_occurs: occurs } = row;
const park = dictionary.get(code) || { code, name, calendar: [] };
park.calendar.push({ event, occurs });
const { code, name, season_type: type, season_start: start, season_end: end } = row;
const park = dictionary.get(code) || { code, name, seasons: [] };
park.seasons.push({ type, start, end });
return dictionary.set(code, park);
}

Expand Down
37 changes: 19 additions & 18 deletions examples/javascript/test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ describe('API', () => {
describe('GET /api/changelog', () => {
it('should respond with changelog for valid projection', async () => {
const { data: changeLog } = await get('api/changelog?projection=park&version=1');
eq(changeLog.length, 8);
eq(changeLog.length, 9);
assertChangeSet(changeLog[0], { id: 1, effective: '2019-01-01T00:00:00.000Z', description: 'Initial Data' });
assertChangeSet(changeLog[7], { id: 8, effective: '2023-01-01T00:00:00.000Z', description: 'Park Calendars - 2023' });
assertChangeSet(changeLog[8], { id: 9, effective: '2024-01-01T00:00:00.000Z', description: '2024 Season' });
});

it('should respond with 400 when missing park query parameter', async () => {
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('API', () => {
it('should redirect to current change set', async () => {
const { status, headers } = await get('api/projection/v1/park');
eq(status, 307);
eq(headers.location, '/api/projection/v1/park?changeSetId=8');
eq(headers.location, '/api/projection/v1/park?changeSetId=9');
});

it('should response with parks for specified change set', async () => {
Expand All @@ -67,18 +67,18 @@ describe('API', () => {
assertPark(parks1[0], { code: 'DC', name: 'Devon Cliffs' });
assertPark(parks1[2], { code: 'PV', name: 'Primrose Valley' });

eq(parks1[0].calendar.length, 4);
assertCalendarEvent(parks1[0].calendar[0], { event: 'Park Open - Owners', occurs: '2019-03-01T00:00:00.000Z' });
assertCalendarEvent(parks1[0].calendar[3], { event: 'Park Close - Owners', occurs: '2019-11-30T00:00:00.000Z' });
eq(parks1[0].seasons.length, 2);
assertSeason(parks1[0].seasons[0], { type: 'Guests', start: '2019-03-15T00:00:00.000Z', end: '2019-11-15T00:00:00.000Z' });
assertSeason(parks1[0].seasons[1], { type: 'Owners', start: '2019-03-01T00:00:00.000Z', end: '2019-11-30T00:00:00.000Z' });

const { data: parks8 } = await get('api/projection/v1/park?changeSetId=8');
eq(parks8.length, 3);
assertPark(parks8[0], { code: 'DC', name: 'Devon Cliffs' });
assertPark(parks8[2], { code: 'SK', name: 'Skegness' });

eq(parks8[0].calendar.length, 8);
assertCalendarEvent(parks8[0].calendar[0], { event: 'Park Open - Owners', occurs: '2022-03-01T00:00:00.000Z' });
assertCalendarEvent(parks8[0].calendar[7], { event: 'Park Close - Owners', occurs: '2023-11-30T00:00:00.000Z' });
eq(parks8[0].seasons.length, 10);
assertSeason(parks8[0].seasons[0], { type: 'Guests', start: '2023-03-15T00:00:00.000Z', end: '2023-11-15T00:00:00.000Z' });
assertSeason(parks8[0].seasons[9], { type: 'Owners', start: '2019-03-01T00:00:00.000Z', end: '2019-11-30T00:00:00.000Z' });
});

it('should respond with 404 when projection does not exist', async () => {
Expand All @@ -103,21 +103,21 @@ describe('API', () => {
it('should redirect to current change set', async () => {
const { status, headers } = await get('api/projection/v1/park/code/DC');
eq(status, 307);
eq(headers.location, '/api/projection/v1/park/code/DC?changeSetId=8');
eq(headers.location, '/api/projection/v1/park/code/DC?changeSetId=9');
});

it('should response with park for specified change set', async () => {
const { data: greenacres } = await get('api/projection/v1/park/code/GA?changeSetId=1');
assertPark(greenacres, { code: 'GA', name: 'Greenacres' });
eq(greenacres.calendar.length, 4);
assertCalendarEvent(greenacres.calendar[0], { event: 'Park Open - Owners', occurs: '2019-03-01T00:00:00.000Z' });
assertCalendarEvent(greenacres.calendar[3], { event: 'Park Close - Owners', occurs: '2019-11-30T00:00:00.000Z' });
eq(greenacres.seasons.length, 2);
assertSeason(greenacres.seasons[0], { type: 'Guests', start: '2019-03-15T00:00:00.000Z', end: '2019-11-15T00:00:00.000Z' });
assertSeason(greenacres.seasons[1], { type: 'Owners', start: '2019-03-01T00:00:00.000Z', end: '2019-11-30T00:00:00.000Z' });

const { data: skegness } = await get('api/projection/v1/park/code/SK?changeSetId=8');
assertPark(skegness, { code: 'SK', name: 'Skegness' });
eq(skegness.calendar.length, 8);
assertCalendarEvent(skegness.calendar[0], { event: 'Park Open - Owners', occurs: '2022-03-01T00:00:00.000Z' });
assertCalendarEvent(skegness.calendar[7], { event: 'Park Close - Owners', occurs: '2023-11-30T00:00:00.000Z' });
eq(skegness.seasons.length, 6);
assertSeason(skegness.seasons[0], { type: 'Guests', start: '2023-03-15T00:00:00.000Z', end: '2023-11-15T00:00:00.000Z' });
assertSeason(skegness.seasons[5], { type: 'Owners', start: '2021-03-01T00:00:00.000Z', end: '2021-11-30T00:00:00.000Z' });
});

it('should response with 404 before park was created', async () => {
Expand Down Expand Up @@ -166,9 +166,10 @@ describe('API', () => {
eq(actual.name, expected.name);
}

function assertCalendarEvent(actual, expected) {
function assertSeason(actual, expected) {
eq(actual.event, expected.event);
eq(actual.occurs, expected.occurs);
eq(actual.start, expected.start);
eq(actual.end, expected.end);
}

async function get(path, options = { method: 'GET', validateStatus: () => true, maxRedirects: 0 }) {
Expand Down
Binary file added examples/migrations/.DS_Store
Binary file not shown.
26 changes: 13 additions & 13 deletions examples/migrations/0001.define-park-schema.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
- operation: ADD_ENUM
name: park_calendar_event_type
name: season_type
values:
- Park Open - Owners
- Park Open - Guests
- Park Close - Owners
- Park Close - Guests
- Owners
- Guests

- operation: ADD_ENTITY
name: park
Expand All @@ -20,19 +18,21 @@
park_code_len: (LENGTH(code) >= 2)

- operation: ADD_ENTITY
name: park_calendar
name: season
version: 1
fields:
- name: id
type: INTEGER
- name: park_code
type: TEXT
- name: event
type: park_calendar_event_type
- name: occurs
- name: type
type: season_type
- name: start
type: TIMESTAMP WITH TIME ZONE
- name: end
type: TIMESTAMP WITH TIME ZONE
identified_by:
- id
- park_code
- type
- start

- operation: ADD_HOOK
name: httpbin/add-projection
Expand All @@ -44,7 +44,7 @@
dependencies:
- entity: park
version: 1
- entity: park_calendar
- entity: season
version: 1

- operation: ADD_HOOK
Expand Down
17 changes: 10 additions & 7 deletions examples/migrations/0002.create-get-park-v1-fn.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@ CREATE FUNCTION get_park_v1(p_change_set_id INTEGER)
RETURNS TABLE (
code TEXT,
name TEXT,
calendar_event park_calendar_event_type,
calendar_occurs TIMESTAMP WITH TIME ZONE
season_type season_type,
season_start TIMESTAMP WITH TIME ZONE,
season_end TIMESTAMP WITH TIME ZONE
)
AS $$
BEGIN
RETURN QUERY
SELECT
p.code,
p.name,
pc.event AS calendar_event,
pc.occurs AS calendar_occurs
s.type AS season_type,
s.start AS season_start,
s.end AS season_end
FROM
get_park_v1_aggregate(p_change_set_id) p
LEFT JOIN
get_park_calendar_v1_aggregate(p_change_set_id) pc ON pc.park_code = p.code
get_season_v1_aggregate(p_change_set_id) s ON s.park_code = p.code
ORDER BY
code ASC,
occurs ASC;
p.code ASC,
s.start DESC,
s.type ASC;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
5 changes: 3 additions & 2 deletions examples/migrations/0003.initial-park-data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
name: Primrose Valley
- code: GA
name: Greenacres
- entity: park_calendar

- entity: season
version: 1
source: ../migrations/csv/park-opening-times-2019.csv
source: ../migrations/csv/season-2019.csv
8 changes: 0 additions & 8 deletions examples/migrations/0004.park-calendars-2020.yaml

This file was deleted.

Loading

0 comments on commit 0471539

Please sign in to comment.