Skip to content

Commit

Permalink
Merge pull request #11 from icefoganalytics/main
Browse files Browse the repository at this point in the history
Updates from IceFog
  • Loading branch information
datajohnson authored Mar 13, 2024
2 parents 1cd1b46 + 4ff3d9b commit 5c8fe30
Show file tree
Hide file tree
Showing 74 changed files with 2,051 additions and 200 deletions.
Binary file modified _Design/Entity Relationship Diagrams.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion _Design/Entity Relationship Diagrams.wsd
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ entity "users" {
division : nvarchar(100)
branch : nvarchar(100)
unit : nvarchar(100)
last_employee_directory_sync_at: datetime2(0)
last_sync_success_at: datetime2(0)
last_sync_failure_at: datetime2(0)
created_at : datetime2(0)
updated_at : datetime2(0)
deleted_at : datetime2(0)
Expand Down
17 changes: 14 additions & 3 deletions api/src/controllers/access-requests-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WhereOptions } from "sequelize"
import { isNil } from "lodash"
import { isEmpty, isNil } from "lodash"

import { AccessRequest, Dataset } from "@/models"
import { TableSerializer } from "@/serializers/access-requests"
Expand All @@ -12,9 +12,19 @@ import BaseController from "@/controllers/base-controller"
export class AccessRequestsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<AccessRequest>
const filters = this.query.filters as Record<string, unknown>

const totalCount = await AccessRequest.count({ where })
const accessRequests = await AccessRequest.findAll({
const scopedAccessRequests = AccessRequestsPolicy.applyScope(AccessRequest, this.currentUser)

let filteredAccessRequests = scopedAccessRequests
if (!isEmpty(filters)) {
Object.entries(filters).forEach(([key, value]) => {
filteredAccessRequests = filteredAccessRequests.scope({ method: [key, value] })
})
}

const totalCount = await filteredAccessRequests.count({ where })
const accessRequests = await filteredAccessRequests.findAll({
where,
include: [
{
Expand All @@ -27,6 +37,7 @@ export class AccessRequestsController extends BaseController {
],
},
"accessGrant",
{ association: "dataset", include: ["integration"] },
],
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
16 changes: 8 additions & 8 deletions api/src/controllers/current-user-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Users } from "@/services"
import { SyncService } from "@/services/users"
import { UserSerializers } from "@/serializers"

import BaseController from "@/controllers/base-controller"
Expand All @@ -11,13 +11,13 @@ export class CurrentUserController extends BaseController {
return this.response.status(200).json({ user: serializedUser })
}

return Users.YukonGovernmentDirectorySyncService.perform(this.currentUser).then(
(updatedUser) => {
// TODO: consider changing interface to Users.AsDetailedSerializer.perform()?
const serializedUser = UserSerializers.asDetailed(updatedUser)
return this.response.status(200).json({ user: serializedUser })
}
)
try {
const updatedUser = await SyncService.perform(this.currentUser)
const serializedUser = UserSerializers.asDetailed(updatedUser)
return this.response.status(200).json({ user: serializedUser })
} catch (error) {
return this.response.status(422).json({ message: `Failed to sync user: ${error}` })
}
}
}

Expand Down
14 changes: 11 additions & 3 deletions api/src/controllers/datasets-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WhereOptions } from "sequelize"
import { isNil } from "lodash"
import { isEmpty, isNil } from "lodash"

import { Dataset } from "@/models"
import { DatasetsPolicy } from "@/policies"
Expand All @@ -11,11 +11,19 @@ import BaseController from "@/controllers/base-controller"
export class DatasetsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<Dataset>
const filters = this.query.filters as Record<string, unknown>

const scopedDatasets = DatasetsPolicy.applyScope(Dataset, this.currentUser)

const totalCount = await scopedDatasets.count({ where })
const datasets = await scopedDatasets.findAll({
let filteredDatasets = scopedDatasets
if (!isEmpty(filters)) {
Object.entries(filters).forEach(([key, value]) => {
filteredDatasets = filteredDatasets.scope({ method: [key, value] })
})
}

const totalCount = await filteredDatasets.count({ where })
const datasets = await filteredDatasets.findAll({
where,
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
13 changes: 10 additions & 3 deletions api/src/controllers/tags-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WhereOptions } from "sequelize"
import { ModelStatic, WhereOptions } from "sequelize"
import { isEmpty } from "lodash"

import { Tag } from "@/models"

Expand All @@ -7,9 +8,15 @@ import BaseController from "@/controllers/base-controller"
export class TagsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<Tag>
const searchToken = this.query.searchToken as string

const totalCount = await Tag.count({ where })
const tags = await Tag.findAll({
let filteredTags: ModelStatic<Tag> = Tag
if (!isEmpty(searchToken)) {
filteredTags = Tag.scope({ method: ["search", searchToken] })
}

const totalCount = await filteredTags.count({ where })
const tags = await filteredTags.findAll({
where,
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
2 changes: 1 addition & 1 deletion api/src/controllers/user-groups/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { YukonGovernmentDirectorySyncController } from "./yukon-government-directory-sync-controller"
export { SyncController } from "./sync-controller"
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { UserGroup } from "@/models"
import { UserGroups } from "@/services"
import { SyncService } from "@/services/user-groups"
import { UserGroupsPolicy } from "@/policies"
import BaseController from "@/controllers/base-controller"

export class YukonGovernmentDirectorySyncController extends BaseController {
export class SyncController extends BaseController {
async create() {
const userGroup = await this.buildUserGroup()
const policy = this.buildPolicy(userGroup)
Expand All @@ -13,8 +13,12 @@ export class YukonGovernmentDirectorySyncController extends BaseController {
.json({ message: "You are not authorized to sync user groups." })
}

const userGroups = await UserGroups.YukonGovernmentDirectorySyncService.perform()
return this.response.status(201).json({ userGroups })
try {
const userGroups = await SyncService.perform()
return this.response.status(201).json({ userGroups })
} catch (error) {
return this.response.status(422).json({ message: `Failed to sync user groups: ${error}` })
}
}

private async buildUserGroup(): Promise<UserGroup> {
Expand All @@ -26,4 +30,4 @@ export class YukonGovernmentDirectorySyncController extends BaseController {
}
}

export default YukonGovernmentDirectorySyncController
export default SyncController
24 changes: 24 additions & 0 deletions api/src/controllers/users-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isNil } from "lodash"
import { User } from "@/models"
import { UserSerializers } from "@/serializers"
import { UsersPolicy } from "@/policies"
import { UpdateService } from "@/services/users"

import BaseController from "@/controllers/base-controller"

Expand Down Expand Up @@ -51,6 +52,29 @@ export class UsersController extends BaseController {
}
}

async update() {
const user = await this.loadUser()
if (isNil(user)) {
return this.response.status(404).json({ message: "User not found." })
}

const policy = this.buildPolicy(user)
if (!policy.update()) {
return this.response
.status(403)
.json({ message: "You are not authorized to update this user." })
}

const permittedAttributes = policy.permitAttributesForUpdate(this.request.body)
try {
const updatedUser = await UpdateService.perform(user, permittedAttributes, this.currentUser)
const serializedUser = UserSerializers.asDetailed(updatedUser)
return this.response.status(200).json({ user: serializedUser })
} catch (error) {
return this.response.status(422).json({ message: `User update failed: ${error}` })
}
}

private async loadUser() {
return User.findByPk(this.params.userId, {
include: [
Expand Down
2 changes: 1 addition & 1 deletion api/src/controllers/users/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { SearchController } from "./search-controller"
export { YukonGovernmentDirectorySyncController } from "./yukon-government-directory-sync-controller"
export { SyncController } from "./sync-controller"
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { isNil } from "lodash"

import { User } from "@/models"
import { Users } from "@/services"
import { SyncService } from "@/services/users"
import { UserSerializers } from "@/serializers"
import { UsersPolicy } from "@/policies"
import BaseController from "@/controllers/base-controller"

export class YukonGovernmentDirectorySyncController extends BaseController {
export class SyncController extends BaseController {
async create() {
const user = await this.loadUser()
if (isNil(user)) return this.response.status(404).json({ message: "User not found." })
Expand All @@ -18,10 +18,13 @@ export class YukonGovernmentDirectorySyncController extends BaseController {
.json({ message: "You are not authorized to sync this user." })
}

return Users.YukonGovernmentDirectorySyncService.perform(this.currentUser).then((user) => {
const serializedUser = UserSerializers.asDetailed(user)
try {
const updatedUser = await SyncService.perform(user)
const serializedUser = UserSerializers.asDetailed(updatedUser)
return this.response.status(201).json({ user: serializedUser })
})
} catch (error) {
return this.response.status(422).json({ message: `Failed to sync user: ${error}` })
}
}

private loadUser(): Promise<User | null> {
Expand All @@ -33,4 +36,4 @@ export class YukonGovernmentDirectorySyncController extends BaseController {
}
}

export default YukonGovernmentDirectorySyncController
export default SyncController
10 changes: 3 additions & 7 deletions api/src/integrations/yukon-government-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,11 @@ export const yukonGovernmentIntegration = {
})
return data
},
async fetchEmpolyee(email: string): Promise<{
employee: YukonGovernmentEmployee
}> {
async fetchEmpolyee(email: string): Promise<YukonGovernmentEmployee | null> {
const { employees } = await yukonGovernmentIntegration.searchEmployees({ email })

if (isEmpty(employees)) {
throw new Error(
`Failed to find any employee info at https://api.gov.yk.ca/directory/employees?email=${email}`
)
return null
}

if (employees.length > 1) {
Expand All @@ -74,7 +70,7 @@ export const yukonGovernmentIntegration = {
throw new Error(errorMessage)
}

return { employee: employees[0] }
return employees[0]
},
async fetchDepartments(): Promise<{
departments: string[]
Expand Down
14 changes: 14 additions & 0 deletions api/src/models/access-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@ AccessRequest.init(
}
},
},
scopes: {
withDatasetOwnerId(ownerId: number) {
return {
include: [
{
association: "dataset",
where: {
ownerId,
},
},
],
}
},
},
}
)

Expand Down
31 changes: 31 additions & 0 deletions api/src/models/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,37 @@ Dataset.init(
},
}
},
withTagNames(tagNames: string[]) {
return {
include: [
{
association: "tags",
where: {
name: {
[Op.in]: tagNames,
},
},
},
],
}
},
withOwnerDepartment(departmentId: number) {
return {
include: [
{
association: "owner",
include: [
{
association: "groupMembership",
where: {
departmentId,
},
},
],
},
],
}
},
},
}
)
Expand Down
14 changes: 13 additions & 1 deletion api/src/models/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
InferCreationAttributes,
Model,
NonAttribute,
Op,
col,
fn,
where,
} from "sequelize"

import sequelize from "@/db/db-client"
Expand Down Expand Up @@ -92,7 +96,15 @@ Tag.init(
deleted_at: null,
},
},
]
],
scopes: {
search(searchToken: string) {
const cleanSearchToken = searchToken.toLowerCase()
return {
where: where(fn("LOWER", col("name")), { [Op.like]: `%${cleanSearchToken}%` }),
}
},
},
}
)

Expand Down
1 change: 1 addition & 0 deletions api/src/models/user-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum UserGroupTypes {
UNIT = "unit",
}

export const UNASSIGNED_USER_GROUP_NAME = "Unassigned"
export const DEFAULT_ORDER = -1

export class UserGroup extends Model<
Expand Down
21 changes: 20 additions & 1 deletion api/src/policies/access-requests-policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NonAttribute } from "sequelize"
import { ModelStatic, NonAttribute } from "sequelize"
import { isNil } from "lodash"

import { Path } from "@/utils/deep-pick"
Expand Down Expand Up @@ -48,6 +48,25 @@ export class AccessRequestsPolicy extends BasePolicy<
return this.datasetsPolicy.update()
}

static applyScope(
modelClass: ModelStatic<AccessRequest>,
user: User
): ModelStatic<AccessRequest> {
if (user.isSystemAdmin || user.isBusinessAnalyst) {
return modelClass
}

if (user.isDataOwner) {
return modelClass.scope({ method: ["withDatasetOwnerId", user.id] })
}

return modelClass.scope({
where: {
requestorId: user.id,
},
})
}

permittedAttributesForUpdate(): Path[] {
return ["denialReason"]
}
Expand Down
Loading

0 comments on commit 5c8fe30

Please sign in to comment.