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

Feature/update stream logic #546

Merged
merged 15 commits into from
Feb 28, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.3.7 (2024-02-xx)
### Common
* **core**: Add `hidden` column to `streams`
* **core**: `hidden` stream won't be calculated to max/min lat/long project
* **core**: `null` stream lat/long won't be calculated to max/min lat/long project

## 1.3.6 (2024-02-xx)
### Common
* **core**: Change Production Arbimon API prefix from `api` to `legacy-api`
Expand Down
4 changes: 4 additions & 0 deletions DEPLOYMENT_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# API Deployment Notes
## 1.3.7
- Run `npm run migrate:core` to apply migrations:
- `20240227000001-add-hidden-to-streams`
- `20240228000001-latitude-longitude-0-to-null`

## 1.3.1
- Run `npm run migrate:core` to apply migrations:
Expand Down
14 changes: 14 additions & 0 deletions core/_cli/migrations/20240227000001-add-hidden-to-streams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
// Add hidden
return queryInterface.addColumn('streams', 'hidden', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('streams', 'hidden')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.query('update streams set latitude = null, longitude = null where latitude = 0 and longitude = 0;')
},
down: (queryInterface) => {
return queryInterface.sequelize.query('update streams set latitude = 0, longitude = 0 where latitude is null and longitude is null;')
}
}
10 changes: 10 additions & 0 deletions core/_docs/requestBodies.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,11 @@
"type": "string",
"description": "A project id",
"example": "qwe2jDgX49"
},
"hidden": {
"type": "boolean",
"description": "Whether stream is hidden (default false)",
"example": "true"
}
},
"required": [
Expand Down Expand Up @@ -493,6 +498,11 @@
"type": "boolean",
"description": "Whether stream is public",
"example": false
},
"hidden": {
"type": "boolean",
"description": "Whether stream is hidden (default false)",
"example": "true"
}
},
"required": [
Expand Down
46 changes: 38 additions & 8 deletions core/_models/streams/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ module.exports = function (sequelize, DataTypes) {
countryCode: {
type: DataTypes.STRING(2),
allowNull: true
},
hidden: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false
}
}, {
paranoid: true,
Expand All @@ -101,14 +106,16 @@ module.exports = function (sequelize, DataTypes) {
},
afterUpdate: async (stream, option) => {
if (stream.projectId !== stream._previousDataValues.projectId) {
await updateMinMaxLatLngFromUpdate(stream.projectId)
await updateMinMaxLatLngFromUpdate(stream._previousDataValues.projectId)
await updateMinMaxLatLngFromUpdate(stream)
await updateMinMaxLatLngFromUpdate(stream._previousDataValues)
} else if (stream.latitude !== stream._previousDataValues.latitude || stream.longitude !== stream._previousDataValues.longitude) {
await updateMinMaxLatLngFromUpdate(stream.projectId)
await updateMinMaxLatLngFromUpdate(stream)
} else if (stream.hidden !== stream._previousDataValues.hidden) {
await updateMinMaxLatLngFromUpdate(stream)
}
},
afterDestroy: async (stream, option) => {
await updateMinMaxLatLngFromUpdate(stream.projectId)
await updateMinMaxLatLngFromUpdate(stream)
}
}
})
Expand All @@ -118,6 +125,12 @@ module.exports = function (sequelize, DataTypes) {
if (!projectId) {
return
}
if (stream.latitude === null || stream.longitude === null) {
return
}
if (stream.hidden) {
return
}
const project = await sequelize.models.Project.findByPk(projectId)
const update = {}
if (project.minLatitude === null || stream.latitude < project.minLatitude) {
Expand All @@ -137,10 +150,12 @@ module.exports = function (sequelize, DataTypes) {
}
}

async function updateMinMaxLatLngFromUpdate (projectId) {
async function updateMinMaxLatLngFromUpdate (stream) {
const projectId = stream.projectId
if (!projectId) {
return
}

const update = await sequelize.models.Stream.findAll({
plain: true,
raw: true,
Expand All @@ -150,7 +165,22 @@ module.exports = function (sequelize, DataTypes) {
[sequelize.fn('min', sequelize.col('longitude')), 'minLongitude'],
[sequelize.fn('max', sequelize.col('longitude')), 'maxLongitude']
],
where: { projectId }
where: {
projectId,
hidden: false,
[sequelize.Sequelize.Op.and]: [
{
latitude: {
[sequelize.Sequelize.Op.not]: null
}
},
{
longitude: {
[sequelize.Sequelize.Op.not]: null
}
}
]
}
})
await sequelize.models.Project.update(update, { where: { id: projectId } })
}
Expand All @@ -160,8 +190,8 @@ module.exports = function (sequelize, DataTypes) {
Stream.belongsTo(models.User, { as: 'created_by', foreignKey: 'created_by_id' })
}
Stream.attributes = {
full: ['id', 'name', 'description', 'start', 'end', 'project_id', 'is_public', 'latitude', 'longitude', 'altitude', 'timezone', 'timezone_locked', 'max_sample_rate', 'external_id', 'created_by_id', 'country_code', 'created_at', 'updated_at'],
lite: ['id', 'name', 'start', 'end', 'latitude', 'longitude', 'altitude', 'is_public']
full: ['id', 'name', 'description', 'start', 'end', 'project_id', 'is_public', 'hidden', 'latitude', 'longitude', 'altitude', 'timezone', 'timezone_locked', 'max_sample_rate', 'external_id', 'created_by_id', 'country_code', 'created_at', 'updated_at'],
lite: ['id', 'name', 'start', 'end', 'latitude', 'longitude', 'altitude', 'is_public', 'hidden']
}
Stream.include = includeBuilder(Stream, 'stream', Stream.attributes.lite)
return Stream
Expand Down
4 changes: 2 additions & 2 deletions core/_services/arbimon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function updateProject (opts, idToken) {

function createSite (stream, idToken) {
const body = {};
['name', 'latitude', 'longitude', 'altitude'].forEach((attr) => { body[attr] = stream[attr] })
['name', 'latitude', 'longitude', 'altitude', 'hidden'].forEach((attr) => { body[attr] = stream[attr] })
body.external_id = stream.id
if (!body.altitude) {
body.altitude = 0
Expand All @@ -69,7 +69,7 @@ function createSite (stream, idToken) {

function updateSite (opts, idToken) {
const body = {};
['name', 'latitude', 'longitude', 'altitude', 'project_id'].forEach((attr) => { body[attr] = opts[attr] })
['name', 'latitude', 'longitude', 'altitude', 'project_id', 'hidden'].forEach((attr) => { body[attr] = opts[attr] })
const options = {
method: 'PATCH',
url: `${arbimonBaseUrl}${arbimonAPIPrefix}integration/sites/${opts.id}`,
Expand Down
128 changes: 126 additions & 2 deletions core/streams/create.int.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('POST /streams', () => {
const stream = await models.Stream.findByPk(id)
expect(stream.timezone).toBe('Asia/Bangkok')
})
test('returns 201 with null latitude and longitude cause null timezone value', async () => {
test('returns 201 with null latitude and longitude cause UTC timezone value', async () => {
const project = { id: 'foo', name: 'my project', createdById: seedValues.otherUserId }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
Expand All @@ -165,7 +165,7 @@ describe('POST /streams', () => {
expect(response.statusCode).toBe(201)
const id = response.header.location.replace('/streams/', '')
const stream = await models.Stream.findByPk(id)
expect(stream.timezone).toBe(null)
expect(stream.timezone).toBe('UTC')
})

test('returns 201 when defined id in request body', async () => {
Expand All @@ -183,6 +183,20 @@ describe('POST /streams', () => {
expect(stream.id).toBe(definedId)
})

test('returns 201 when hidden in request body', async () => {
const requestBody = {
name: 'test-stream-with-id',
hidden: true
}

const response = await request(app).post('/').send(requestBody)

expect(response.statusCode).toBe(201)
const id = response.header.location.replace('/streams/', '')
const stream = await models.Stream.findByPk(id)
expect(stream.hidden).toBe(requestBody.hidden)
})

test('returns 400 when id length less than minimum(12)', async () => {
const requestBody = {
id: 'abcdef12345',
Expand Down Expand Up @@ -312,4 +326,114 @@ describe('POST /streams', () => {
expect(stream.id).toBe('qwertyuiop40')
expect(stream.countryCode).toBe(null)
})

test('min/max latitude/longitude of project change when add stream', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: 54.2, longitude: -4.5, projectId: project.id }
await models.Stream.create(stream)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBe(stream.latitude)
expect(projectAfterCreated.maxLatitude).toBe(stream.latitude)
expect(projectAfterCreated.minLongitude).toBe(stream.longitude)
expect(projectAfterCreated.maxLongitude).toBe(stream.longitude)
})

test('min/max latitude/longitude of project change when add 2 stream', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: 54.2, longitude: -4.5, projectId: project.id }
const stream2 = { id: 'jagu2', createdById: seedValues.primaryUserId, name: 'Jaguar Station 2', latitude: 66.2, longitude: -10.5, projectId: project.id }
await models.Stream.create(stream)
await models.Stream.create(stream2)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBe(stream.latitude)
expect(projectAfterCreated.maxLatitude).toBe(stream2.latitude)
expect(projectAfterCreated.minLongitude).toBe(stream2.longitude)
expect(projectAfterCreated.maxLongitude).toBe(stream.longitude)
})

test('min/max latitude/longitude of project change when add 2 stream with 1 hidden', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: 54.2, longitude: -4.5, projectId: project.id }
const stream2 = { id: 'jagu2', createdById: seedValues.primaryUserId, name: 'Jaguar Station 2', latitude: 66.2, longitude: -10.5, projectId: project.id, hidden: true }
await models.Stream.create(stream)
await models.Stream.create(stream2)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBe(stream.latitude)
expect(projectAfterCreated.maxLatitude).toBe(stream.latitude)
expect(projectAfterCreated.minLongitude).toBe(stream.longitude)
expect(projectAfterCreated.maxLongitude).toBe(stream.longitude)
})

test('min/max latitude/longitude of project change when add 2 stream with 2 hidden', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: 54.2, longitude: -4.5, projectId: project.id, hidden: true }
const stream2 = { id: 'jagu2', createdById: seedValues.primaryUserId, name: 'Jaguar Station 2', latitude: 66.2, longitude: -10.5, projectId: project.id, hidden: true }
await models.Stream.create(stream)
await models.Stream.create(stream2)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBeNull()
expect(projectAfterCreated.maxLatitude).toBeNull()
expect(projectAfterCreated.minLongitude).toBeNull()
expect(projectAfterCreated.maxLongitude).toBeNull()
})

test('min/max latitude/longitude of project change when add stream with null latitude', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: null, longitude: -4.5, projectId: project.id }
await models.Stream.create(stream)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBeNull()
expect(projectAfterCreated.maxLatitude).toBeNull()
expect(projectAfterCreated.minLongitude).toBeNull()
expect(projectAfterCreated.maxLongitude).toBeNull()
})

test('min/max latitude/longitude of project change when add 2 stream with 1 null latitude', async () => {
const project = { id: 'p123p', createdById: seedValues.primaryUserId, name: 'Primary User Project' }
await models.Project.create(project)
await models.UserProjectRole.create({ user_id: seedValues.primaryUserId, project_id: project.id, role_id: seedValues.roleAdmin })
const stream = { id: 'jagu1', createdById: seedValues.primaryUserId, name: 'Jaguar Station', latitude: 54.2, longitude: -4.5, projectId: project.id }
const stream2 = { id: 'jagu2', createdById: seedValues.primaryUserId, name: 'Jaguar Station 2', latitude: null, longitude: -10.5, projectId: project.id }
await models.Stream.create(stream)
await models.Stream.create(stream2)

const response = await request(app).get('/').query({ name: 'Jaguar Station' })

expect(response.statusCode).toBe(200)
const projectAfterCreated = await models.Project.findByPk(project.id)
expect(projectAfterCreated.minLatitude).toBe(stream.latitude)
expect(projectAfterCreated.maxLatitude).toBe(stream.latitude)
expect(projectAfterCreated.minLongitude).toBe(stream.longitude)
expect(projectAfterCreated.maxLongitude).toBe(stream.longitude)
})
})
9 changes: 9 additions & 0 deletions core/streams/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports = (req, res) => {
converter.convert('is_public').optional().toBoolean().default(false)
converter.convert('external_id').optional().toInt()
converter.convert('project_id').optional().toString()
converter.convert('hidden').optional().toBoolean()

return converter.validate()
.then(async (params) => {
Expand All @@ -57,6 +58,14 @@ module.exports = (req, res) => {
stream.id = randomId()
}

if (params.latitude === 0) {
params.latitude = null
}

if (params.longitude === 0) {
params.longitude = null
}

if (params.projectId) {
const duplicateStreamInProject = await dao.query({ names: [params.name], projects: [params.projectId] }, { fields: 'id' })
if (duplicateStreamInProject.total > 0) {
Expand Down
10 changes: 9 additions & 1 deletion core/streams/dao/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const availableIncludes = [
function computedAdditions (data, stream = {}) {
const additions = {}
const { latitude, longitude } = data
if (latitude !== undefined && longitude !== undefined) {
if (latitude && longitude) {
const country = crg.get_country(latitude, longitude)
if (country) {
additions.countryName = country.name
Expand All @@ -26,6 +26,8 @@ function computedAdditions (data, stream = {}) {
additions.timezone = getTzByLatLng(latitude, longitude)
}
additions.countryCode = getCountryCodeByLatLng(latitude, longitude)
} else {
additions.timezone = 'UTC'
}
return additions
}
Expand Down Expand Up @@ -104,6 +106,7 @@ async function create (stream, options = {}) {
* @param {number} options.offset Number of results to skip
* @param {string} options.permission Include only streams for which you have selected permission (only 'C', 'R', 'U', 'D' are available)
* @param {number} options.permissableBy Include only streams permissable by the given user id
* @param {number} options.hidden Include only hidden streams
*/
async function query (filters, options = {}) {
const where = {}
Expand Down Expand Up @@ -181,6 +184,11 @@ async function query (filters, options = {}) {
}
}

// Not include hidden streams by default
if (options.hidden != null) {
where.hidden = options.hidden
}

const attributes = options.fields && options.fields.length > 0 ? Stream.attributes.full.filter(a => options.fields.includes(a)) : Stream.attributes.lite
const include = options.fields && options.fields.length > 0 ? availableIncludes.filter(i => options.fields.includes(i.as)) : []
const order = getSortFields(options.sort || '-updated_at')
Expand Down
Loading
Loading