generated from alphane/template
Adding in flashcard's and decks
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import logger from "@/utils/logger"
|
||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import { FlashcardDecksPolicy } from "@/policies"
|
||||||
|
import { CreateService, DestroyService, UpdateService } from "@/services/flashcard-decks"
|
||||||
|
import { IndexSerializer, ShowSerializer } from "@/serializers/flashcard-decks"
|
||||||
|
import BaseController from "@/controllers/base-controller"
|
||||||
|
|
||||||
|
export class FlashcardDecksController extends BaseController<FlashcardDeck> {
|
||||||
|
async index() {
|
||||||
|
try {
|
||||||
|
const where = this.buildWhere()
|
||||||
|
const scopes = this.buildFilterScopes()
|
||||||
|
const scopedDecks = FlashcardDecksPolicy.applyScope(scopes, this.currentUser)
|
||||||
|
|
||||||
|
const totalCount = await scopedDecks.count({ where })
|
||||||
|
const flashcardDecks = await scopedDecks.findAll({
|
||||||
|
where,
|
||||||
|
limit: this.pagination.limit,
|
||||||
|
offset: this.pagination.offset,
|
||||||
|
order: this.buildOrder(),
|
||||||
|
})
|
||||||
|
const serializedDecks = IndexSerializer.perform(flashcardDecks)
|
||||||
|
return this.response.json({ flashcardDecks: serializedDecks, totalCount })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching flashcard decks" + error)
|
||||||
|
return this.response.status(400).json({ message: `Error fetching flashcard decks: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async show() {
|
||||||
|
try {
|
||||||
|
const flashcardDeck = await this.loadFlashcardDeck()
|
||||||
|
if (isNil(flashcardDeck)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard deck not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcardDeck)
|
||||||
|
if (!policy.show()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to view this flashcard deck" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedDeck = ShowSerializer.perform(flashcardDeck)
|
||||||
|
return this.response.json({ flashcardDeck: serializedDeck, policy })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching flashcard deck" + error)
|
||||||
|
return this.response.status(400).json({ message: `Error fetching flashcard deck: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
try {
|
||||||
|
const policy = this.buildPolicy()
|
||||||
|
if (!policy.create()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to create flashcard decks" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const permittedAttributes = policy.permitAttributesForCreate(this.request.body)
|
||||||
|
const flashcardDeck = await CreateService.perform({
|
||||||
|
...permittedAttributes,
|
||||||
|
creatorId: this.currentUser.id,
|
||||||
|
})
|
||||||
|
const serializedDeck = ShowSerializer.perform(flashcardDeck)
|
||||||
|
return this.response.status(201).json({ flashcardDeck: serializedDeck })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error creating flashcard deck" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error creating flashcard deck: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
try {
|
||||||
|
const flashcardDeck = await this.loadFlashcardDeck()
|
||||||
|
if (isNil(flashcardDeck)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard deck not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcardDeck)
|
||||||
|
if (!policy.update()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to update this flashcard deck" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const permittedAttributes = policy.permitAttributes(this.request.body)
|
||||||
|
const updatedDeck = await UpdateService.perform(flashcardDeck, permittedAttributes)
|
||||||
|
const serializedDeck = ShowSerializer.perform(updatedDeck)
|
||||||
|
return this.response.json({ flashcardDeck: serializedDeck })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error updating flashcard deck" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error updating flashcard deck: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
try {
|
||||||
|
const flashcardDeck = await this.loadFlashcardDeck()
|
||||||
|
if (isNil(flashcardDeck)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard deck not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcardDeck)
|
||||||
|
if (!policy.destroy()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to delete this flashcard deck" })
|
||||||
|
}
|
||||||
|
|
||||||
|
await DestroyService.perform(flashcardDeck)
|
||||||
|
return this.response.status(204).send()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error deleting flashcard deck" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error deleting flashcard deck: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFlashcardDeck() {
|
||||||
|
return FlashcardDeck.findByPk(this.params.flashcardDeckId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPolicy(flashcardDeck: FlashcardDeck = FlashcardDeck.build()) {
|
||||||
|
return new FlashcardDecksPolicy(this.currentUser, flashcardDeck)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashcardDecksController
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import logger from "@/utils/logger"
|
||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import { FlashcardsPolicy } from "@/policies"
|
||||||
|
import { CreateService, DestroyService, UpdateService } from "@/services/flashcards"
|
||||||
|
import { IndexSerializer, ShowSerializer } from "@/serializers/flashcards"
|
||||||
|
import BaseController from "@/controllers/base-controller"
|
||||||
|
|
||||||
|
export class FlashcardsController extends BaseController<Flashcard> {
|
||||||
|
async index() {
|
||||||
|
try {
|
||||||
|
const where = this.buildWhere()
|
||||||
|
const scopes = this.buildFilterScopes()
|
||||||
|
const scopedFlashcards = FlashcardsPolicy.applyScope(scopes, this.currentUser)
|
||||||
|
|
||||||
|
const totalCount = await scopedFlashcards.count({ where })
|
||||||
|
const flashcards = await scopedFlashcards.findAll({
|
||||||
|
where,
|
||||||
|
limit: this.pagination.limit,
|
||||||
|
offset: this.pagination.offset,
|
||||||
|
order: this.buildOrder(),
|
||||||
|
})
|
||||||
|
const serializedFlashcards = IndexSerializer.perform(flashcards)
|
||||||
|
return this.response.json({ flashcards: serializedFlashcards, totalCount })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching flashcards" + error)
|
||||||
|
return this.response.status(400).json({ message: `Error fetching flashcards: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async show() {
|
||||||
|
try {
|
||||||
|
const flashcard = await this.loadFlashcard()
|
||||||
|
if (isNil(flashcard)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcard)
|
||||||
|
if (!policy.show()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to view this flashcard" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedFlashcard = ShowSerializer.perform(flashcard)
|
||||||
|
return this.response.json({ flashcard: serializedFlashcard, policy })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching flashcard" + error)
|
||||||
|
return this.response.status(400).json({ message: `Error fetching flashcard: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
try {
|
||||||
|
const policy = this.buildPolicy()
|
||||||
|
if (!policy.create()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to create flashcards" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const permittedAttributes = policy.permitAttributesForCreate(this.request.body)
|
||||||
|
const flashcard = await CreateService.perform({
|
||||||
|
...permittedAttributes,
|
||||||
|
creatorId: this.currentUser.id,
|
||||||
|
})
|
||||||
|
const serializedFlashcard = ShowSerializer.perform(flashcard)
|
||||||
|
return this.response.status(201).json({ flashcard: serializedFlashcard })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error creating flashcard" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error creating flashcard: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
try {
|
||||||
|
const flashcard = await this.loadFlashcard()
|
||||||
|
if (isNil(flashcard)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcard)
|
||||||
|
if (!policy.update()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to update this flashcard" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const permittedAttributes = policy.permitAttributes(this.request.body)
|
||||||
|
const updatedFlashcard = await UpdateService.perform(flashcard, permittedAttributes)
|
||||||
|
const serializedFlashcard = ShowSerializer.perform(updatedFlashcard)
|
||||||
|
return this.response.json({ flashcard: serializedFlashcard })
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error updating flashcard" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error updating flashcard: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
try {
|
||||||
|
const flashcard = await this.loadFlashcard()
|
||||||
|
if (isNil(flashcard)) {
|
||||||
|
return this.response.status(404).json({ message: "Flashcard not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = this.buildPolicy(flashcard)
|
||||||
|
if (!policy.destroy()) {
|
||||||
|
return this.response.status(403).json({ message: "You are not authorized to delete this flashcard" })
|
||||||
|
}
|
||||||
|
|
||||||
|
await DestroyService.perform(flashcard)
|
||||||
|
return this.response.status(204).send()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error deleting flashcard" + error)
|
||||||
|
return this.response.status(422).json({ message: `Error deleting flashcard: ${error}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFlashcard() {
|
||||||
|
return Flashcard.findByPk(this.params.flashcardId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPolicy(flashcard: Flashcard = Flashcard.build()) {
|
||||||
|
return new FlashcardsPolicy(this.currentUser, flashcard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashcardsController
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
// Controllers
|
// Controllers
|
||||||
export { CurrentUserController } from "./current-user-controller"
|
export { CurrentUserController } from "./current-user-controller"
|
||||||
|
export { FlashcardDecksController } from "./flashcard-decks-controller"
|
||||||
|
export { FlashcardsController } from "./flashcards-controller"
|
||||||
export { UsersController } from "./users-controller"
|
export { UsersController } from "./users-controller"
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Knex } from "knex"
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable("flashcard_decks", function (table) {
|
||||||
|
table.increments("id").notNullable().primary()
|
||||||
|
table.integer("parent_deck_id").nullable()
|
||||||
|
table.integer("creator_id").notNullable()
|
||||||
|
table.string("name", 255).notNullable()
|
||||||
|
|
||||||
|
table
|
||||||
|
.specificType("created_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
.notNullable()
|
||||||
|
.defaultTo(knex.raw("CURRENT_TIMESTAMP(0)"))
|
||||||
|
table
|
||||||
|
.specificType("updated_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
.notNullable()
|
||||||
|
.defaultTo(knex.raw("CURRENT_TIMESTAMP(0)"))
|
||||||
|
table.specificType("deleted_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
|
||||||
|
table.foreign("parent_deck_id").references("flashcard_decks.id")
|
||||||
|
table.foreign("creator_id").references("users.id")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTable("flashcard_decks")
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Knex } from "knex"
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable("flashcards", function (table) {
|
||||||
|
table.increments("id").notNullable().primary()
|
||||||
|
table.integer("flashcard_deck_id").notNullable()
|
||||||
|
table.integer("creator_id").notNullable()
|
||||||
|
table.string("card_type", 255).notNullable()
|
||||||
|
table.text("front").notNullable()
|
||||||
|
table.text("back").nullable()
|
||||||
|
|
||||||
|
table
|
||||||
|
.specificType("created_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
.notNullable()
|
||||||
|
.defaultTo(knex.raw("CURRENT_TIMESTAMP(0)"))
|
||||||
|
table
|
||||||
|
.specificType("updated_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
.notNullable()
|
||||||
|
.defaultTo(knex.raw("CURRENT_TIMESTAMP(0)"))
|
||||||
|
table.specificType("deleted_at", "TIMESTAMP WITH TIME ZONE")
|
||||||
|
|
||||||
|
table.foreign("flashcard_deck_id").references("flashcard_decks.id")
|
||||||
|
table.foreign("creator_id").references("users.id")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTable("flashcards")
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
type CreationOptional,
|
||||||
|
DataTypes,
|
||||||
|
InferAttributes,
|
||||||
|
InferCreationAttributes,
|
||||||
|
type NonAttribute,
|
||||||
|
sql,
|
||||||
|
} from "@sequelize/core"
|
||||||
|
import {
|
||||||
|
Attribute,
|
||||||
|
AutoIncrement,
|
||||||
|
BelongsTo,
|
||||||
|
Default,
|
||||||
|
NotNull,
|
||||||
|
PrimaryKey,
|
||||||
|
} from "@sequelize/core/decorators-legacy"
|
||||||
|
|
||||||
|
import BaseModel from "@/models/base-model"
|
||||||
|
|
||||||
|
export class FlashcardDeck extends BaseModel<
|
||||||
|
InferAttributes<FlashcardDeck>,
|
||||||
|
InferCreationAttributes<FlashcardDeck>
|
||||||
|
> {
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
@PrimaryKey
|
||||||
|
@AutoIncrement
|
||||||
|
declare id: CreationOptional<number>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
declare parentDeckId: number | null
|
||||||
|
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
@NotNull
|
||||||
|
declare creatorId: number
|
||||||
|
|
||||||
|
@Attribute(DataTypes.STRING(255))
|
||||||
|
@NotNull
|
||||||
|
declare name: string
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
@NotNull
|
||||||
|
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||||
|
declare createdAt: CreationOptional<Date>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
@NotNull
|
||||||
|
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||||
|
declare updatedAt: CreationOptional<Date>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
declare deletedAt: Date | null
|
||||||
|
|
||||||
|
// Associations
|
||||||
|
@BelongsTo(() => FlashcardDeck, {
|
||||||
|
foreignKey: {
|
||||||
|
name: "parentDeckId",
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
declare parentDeck?: NonAttribute<FlashcardDeck>
|
||||||
|
|
||||||
|
// Scopes
|
||||||
|
static establishScopes(): void {
|
||||||
|
this.addSearchScope(["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashcardDeck
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
type CreationOptional,
|
||||||
|
DataTypes,
|
||||||
|
InferAttributes,
|
||||||
|
InferCreationAttributes,
|
||||||
|
type NonAttribute,
|
||||||
|
sql,
|
||||||
|
} from "@sequelize/core"
|
||||||
|
import {
|
||||||
|
Attribute,
|
||||||
|
AutoIncrement,
|
||||||
|
BelongsTo,
|
||||||
|
Default,
|
||||||
|
NotNull,
|
||||||
|
PrimaryKey,
|
||||||
|
} from "@sequelize/core/decorators-legacy"
|
||||||
|
|
||||||
|
import BaseModel from "@/models/base-model"
|
||||||
|
import FlashcardDeck from "@/models/flashcard-deck"
|
||||||
|
|
||||||
|
export class Flashcard extends BaseModel<
|
||||||
|
InferAttributes<Flashcard>,
|
||||||
|
InferCreationAttributes<Flashcard>
|
||||||
|
> {
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
@PrimaryKey
|
||||||
|
@AutoIncrement
|
||||||
|
declare id: CreationOptional<number>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
@NotNull
|
||||||
|
declare flashcardDeckId: number
|
||||||
|
|
||||||
|
@Attribute(DataTypes.INTEGER)
|
||||||
|
@NotNull
|
||||||
|
declare creatorId: number
|
||||||
|
|
||||||
|
@Attribute(DataTypes.STRING(255))
|
||||||
|
@NotNull
|
||||||
|
declare cardType: string
|
||||||
|
|
||||||
|
@Attribute(DataTypes.TEXT)
|
||||||
|
@NotNull
|
||||||
|
declare front: string
|
||||||
|
|
||||||
|
@Attribute(DataTypes.TEXT)
|
||||||
|
declare back: string | null
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
@NotNull
|
||||||
|
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||||
|
declare createdAt: CreationOptional<Date>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
@NotNull
|
||||||
|
@Default(sql.literal("CURRENT_TIMESTAMP"))
|
||||||
|
declare updatedAt: CreationOptional<Date>
|
||||||
|
|
||||||
|
@Attribute(DataTypes.DATE(0))
|
||||||
|
declare deletedAt: Date | null
|
||||||
|
|
||||||
|
// Associations
|
||||||
|
@BelongsTo(() => FlashcardDeck, {
|
||||||
|
foreignKey: {
|
||||||
|
name: "flashcardDeckId",
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
inverse: {
|
||||||
|
as: "flashcards",
|
||||||
|
type: "hasMany",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
declare flashcardDeck?: NonAttribute<FlashcardDeck>
|
||||||
|
|
||||||
|
// Scopes
|
||||||
|
static establishScopes(): void {
|
||||||
|
this.addSearchScope(["front", "back"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flashcard
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
import db from "@/db/db-client"
|
import db from "@/db/db-client"
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
|
import Flashcard from "@/models/flashcard"
|
||||||
|
import FlashcardDeck from "@/models/flashcard-deck"
|
||||||
import User, { UserRoles } from "@/models/user"
|
import User, { UserRoles } from "@/models/user"
|
||||||
|
|
||||||
db.addModels([
|
db.addModels([
|
||||||
|
Flashcard,
|
||||||
|
FlashcardDeck,
|
||||||
User,
|
User,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Lazy load scopes
|
// Lazy load scopes
|
||||||
|
Flashcard.establishScopes()
|
||||||
|
FlashcardDeck.establishScopes()
|
||||||
User.establishScopes()
|
User.establishScopes()
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
Flashcard,
|
||||||
|
FlashcardDeck,
|
||||||
User,
|
User,
|
||||||
UserRoles,
|
UserRoles,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Attributes, FindOptions } from "@sequelize/core"
|
||||||
|
|
||||||
|
import { FlashcardDeck, User } from "@/models"
|
||||||
|
import { ALL_RECORDS_SCOPE, PolicyFactory } from "@/policies/base-policy"
|
||||||
|
import { Path } from "@/utils/deep-pick"
|
||||||
|
|
||||||
|
export class FlashcardDecksPolicy extends PolicyFactory(FlashcardDeck) {
|
||||||
|
show(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): boolean {
|
||||||
|
return this.user.id === this.record.creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): boolean {
|
||||||
|
return this.user.id === this.record.creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributes(): Path[] {
|
||||||
|
return ["name", "parentDeckId"] as (keyof Attributes<FlashcardDeck>)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributesForCreate(): Path[] {
|
||||||
|
return [...this.permittedAttributes()]
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributesForUpdate(): Path[] {
|
||||||
|
return [...this.permittedAttributes()]
|
||||||
|
}
|
||||||
|
|
||||||
|
static policyScope(_user: User): FindOptions<Attributes<FlashcardDeck>> {
|
||||||
|
return ALL_RECORDS_SCOPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashcardDecksPolicy
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Attributes, FindOptions } from "@sequelize/core"
|
||||||
|
|
||||||
|
import { Flashcard, User } from "@/models"
|
||||||
|
import { ALL_RECORDS_SCOPE, PolicyFactory } from "@/policies/base-policy"
|
||||||
|
import { Path } from "@/utils/deep-pick"
|
||||||
|
|
||||||
|
export class FlashcardsPolicy extends PolicyFactory(Flashcard) {
|
||||||
|
show(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): boolean {
|
||||||
|
return this.user.id === this.record.creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): boolean {
|
||||||
|
return this.user.id === this.record.creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributes(): Path[] {
|
||||||
|
return ["flashcardDeckId", "cardType", "front", "back"] as (keyof Attributes<Flashcard>)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributesForCreate(): Path[] {
|
||||||
|
return [...this.permittedAttributes()]
|
||||||
|
}
|
||||||
|
|
||||||
|
permittedAttributesForUpdate(): Path[] {
|
||||||
|
return [...this.permittedAttributes()]
|
||||||
|
}
|
||||||
|
|
||||||
|
static policyScope(_user: User): FindOptions<Attributes<Flashcard>> {
|
||||||
|
return ALL_RECORDS_SCOPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlashcardsPolicy
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
// Policy Bundles
|
// Policy Bundles
|
||||||
export { type BaseScopeOptions } from "./base-policy"
|
export { type BaseScopeOptions } from "./base-policy"
|
||||||
|
export { FlashcardDecksPolicy } from "./flashcard-decks-policy"
|
||||||
|
export { FlashcardsPolicy } from "./flashcards-policy"
|
||||||
export { UsersPolicy } from "./users-policy"
|
export { UsersPolicy } from "./users-policy"
|
||||||
|
|||||||
+21
-1
@@ -16,7 +16,7 @@ import { logger } from "@/utils/logger"
|
|||||||
|
|
||||||
import { jwtMiddleware, authorizationMiddleware } from "@/middlewares"
|
import { jwtMiddleware, authorizationMiddleware } from "@/middlewares"
|
||||||
|
|
||||||
import { CurrentUserController, UsersController } from "@/controllers"
|
import { CurrentUserController, FlashcardDecksController, FlashcardsController, UsersController } from "@/controllers"
|
||||||
|
|
||||||
export const router = Router()
|
export const router = Router()
|
||||||
|
|
||||||
@@ -42,6 +42,26 @@ router
|
|||||||
.patch(UsersController.update)
|
.patch(UsersController.update)
|
||||||
.delete(UsersController.destroy)
|
.delete(UsersController.destroy)
|
||||||
|
|
||||||
|
router
|
||||||
|
.route("/api/flashcard-decks")
|
||||||
|
.get(FlashcardDecksController.index)
|
||||||
|
.post(FlashcardDecksController.create)
|
||||||
|
router
|
||||||
|
.route("/api/flashcard-decks/:flashcardDeckId")
|
||||||
|
.get(FlashcardDecksController.show)
|
||||||
|
.patch(FlashcardDecksController.update)
|
||||||
|
.delete(FlashcardDecksController.destroy)
|
||||||
|
|
||||||
|
router
|
||||||
|
.route("/api/flashcards")
|
||||||
|
.get(FlashcardsController.index)
|
||||||
|
.post(FlashcardsController.create)
|
||||||
|
router
|
||||||
|
.route("/api/flashcards/:flashcardId")
|
||||||
|
.get(FlashcardsController.show)
|
||||||
|
.patch(FlashcardsController.update)
|
||||||
|
.delete(FlashcardsController.destroy)
|
||||||
|
|
||||||
// if no other routes match, return a 404
|
// if no other routes match, return a 404
|
||||||
router.use("/api", (req: Request, res: Response) => {
|
router.use("/api", (req: Request, res: Response) => {
|
||||||
return res.status(404).json({ message: "Not Found", url: req.path })
|
return res.status(404).json({ message: "Not Found", url: req.path })
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { pick } from "lodash"
|
||||||
|
|
||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import BaseSerializer from "@/serializers/base-serializer"
|
||||||
|
|
||||||
|
export type FlashcardDeckIndexView = Pick<
|
||||||
|
FlashcardDeck,
|
||||||
|
"id" | "parentDeckId" | "creatorId" | "name" | "createdAt" | "updatedAt"
|
||||||
|
>
|
||||||
|
|
||||||
|
export class IndexSerializer extends BaseSerializer<FlashcardDeck> {
|
||||||
|
perform(): FlashcardDeckIndexView {
|
||||||
|
return pick(this.record, ["id", "parentDeckId", "creatorId", "name", "createdAt", "updatedAt"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexSerializer
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { IndexSerializer } from "./index-serializer"
|
||||||
|
export { ShowSerializer } from "./show-serializer"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { pick } from "lodash"
|
||||||
|
|
||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import BaseSerializer from "@/serializers/base-serializer"
|
||||||
|
|
||||||
|
export type FlashcardDeckShowView = Pick<
|
||||||
|
FlashcardDeck,
|
||||||
|
"id" | "parentDeckId" | "creatorId" | "name" | "createdAt" | "updatedAt"
|
||||||
|
>
|
||||||
|
|
||||||
|
export class ShowSerializer extends BaseSerializer<FlashcardDeck> {
|
||||||
|
perform(): FlashcardDeckShowView {
|
||||||
|
return pick(this.record, ["id", "parentDeckId", "creatorId", "name", "createdAt", "updatedAt"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShowSerializer
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { pick } from "lodash"
|
||||||
|
|
||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import BaseSerializer from "@/serializers/base-serializer"
|
||||||
|
|
||||||
|
export type FlashcardIndexView = Pick<
|
||||||
|
Flashcard,
|
||||||
|
"id" | "flashcardDeckId" | "creatorId" | "cardType" | "front" | "back" | "createdAt" | "updatedAt"
|
||||||
|
>
|
||||||
|
|
||||||
|
export class IndexSerializer extends BaseSerializer<Flashcard> {
|
||||||
|
perform(): FlashcardIndexView {
|
||||||
|
return pick(this.record, [
|
||||||
|
"id",
|
||||||
|
"flashcardDeckId",
|
||||||
|
"creatorId",
|
||||||
|
"cardType",
|
||||||
|
"front",
|
||||||
|
"back",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexSerializer
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { IndexSerializer } from "./index-serializer"
|
||||||
|
export { ShowSerializer } from "./show-serializer"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { pick } from "lodash"
|
||||||
|
|
||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import BaseSerializer from "@/serializers/base-serializer"
|
||||||
|
|
||||||
|
export type FlashcardShowView = Pick<
|
||||||
|
Flashcard,
|
||||||
|
"id" | "flashcardDeckId" | "creatorId" | "cardType" | "front" | "back" | "createdAt" | "updatedAt"
|
||||||
|
>
|
||||||
|
|
||||||
|
export class ShowSerializer extends BaseSerializer<Flashcard> {
|
||||||
|
perform(): FlashcardShowView {
|
||||||
|
return pick(this.record, [
|
||||||
|
"id",
|
||||||
|
"flashcardDeckId",
|
||||||
|
"creatorId",
|
||||||
|
"cardType",
|
||||||
|
"front",
|
||||||
|
"back",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShowSerializer
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
// Bundled exports
|
// Bundled exports
|
||||||
|
export * as FlashcardDecks from "./flashcard-decks"
|
||||||
|
export * as Flashcards from "./flashcards"
|
||||||
export * as Users from "./users"
|
export * as Users from "./users"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { CreationAttributes } from "@sequelize/core"
|
||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export type FlashcardDeckCreationAttributes = Partial<CreationAttributes<FlashcardDeck>>
|
||||||
|
|
||||||
|
export class CreateService extends BaseService {
|
||||||
|
constructor(private attributes: FlashcardDeckCreationAttributes) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<FlashcardDeck> {
|
||||||
|
const { creatorId, name, ...optionalAttributes } = this.attributes
|
||||||
|
|
||||||
|
if (isNil(creatorId)) {
|
||||||
|
throw new Error("Creator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(name)) {
|
||||||
|
throw new Error("Name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlashcardDeck.create({
|
||||||
|
...optionalAttributes,
|
||||||
|
creatorId,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateService
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export class DestroyService extends BaseService {
|
||||||
|
constructor(private flashcardDeck: FlashcardDeck) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<void> {
|
||||||
|
return this.flashcardDeck.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DestroyService
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { CreateService } from "./create-service"
|
||||||
|
export { UpdateService } from "./update-service"
|
||||||
|
export { DestroyService } from "./destroy-service"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Attributes } from "@sequelize/core"
|
||||||
|
|
||||||
|
import { FlashcardDeck } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export type FlashcardDeckUpdateAttributes = Partial<Attributes<FlashcardDeck>>
|
||||||
|
|
||||||
|
export class UpdateService extends BaseService {
|
||||||
|
constructor(
|
||||||
|
private flashcardDeck: FlashcardDeck,
|
||||||
|
private attributes: FlashcardDeckUpdateAttributes
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<FlashcardDeck> {
|
||||||
|
return this.flashcardDeck.update(this.attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateService
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { CreationAttributes } from "@sequelize/core"
|
||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export type FlashcardCreationAttributes = Partial<CreationAttributes<Flashcard>>
|
||||||
|
|
||||||
|
export class CreateService extends BaseService {
|
||||||
|
constructor(private attributes: FlashcardCreationAttributes) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<Flashcard> {
|
||||||
|
const { flashcardDeckId, creatorId, cardType, front, ...optionalAttributes } = this.attributes
|
||||||
|
|
||||||
|
if (isNil(flashcardDeckId)) {
|
||||||
|
throw new Error("Flashcard deck is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(creatorId)) {
|
||||||
|
throw new Error("Creator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(cardType)) {
|
||||||
|
throw new Error("Card type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(front)) {
|
||||||
|
throw new Error("Front is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flashcard.create({
|
||||||
|
...optionalAttributes,
|
||||||
|
flashcardDeckId,
|
||||||
|
creatorId,
|
||||||
|
cardType,
|
||||||
|
front,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateService
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export class DestroyService extends BaseService {
|
||||||
|
constructor(private flashcard: Flashcard) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<void> {
|
||||||
|
return this.flashcard.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DestroyService
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { CreateService } from "./create-service"
|
||||||
|
export { UpdateService } from "./update-service"
|
||||||
|
export { DestroyService } from "./destroy-service"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Attributes } from "@sequelize/core"
|
||||||
|
|
||||||
|
import { Flashcard } from "@/models"
|
||||||
|
import BaseService from "@/services/base-service"
|
||||||
|
|
||||||
|
export type FlashcardUpdateAttributes = Partial<Attributes<Flashcard>>
|
||||||
|
|
||||||
|
export class UpdateService extends BaseService {
|
||||||
|
constructor(
|
||||||
|
private flashcard: Flashcard,
|
||||||
|
private attributes: FlashcardUpdateAttributes
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async perform(): Promise<Flashcard> {
|
||||||
|
return this.flashcard.update(this.attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateService
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
export * as FlashcardDecks from "./flashcard-decks"
|
||||||
|
export * as Flashcards from "./flashcards"
|
||||||
export * as Users from "./users"
|
export * as Users from "./users"
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import http from "@/api/http-client"
|
||||||
|
import { type FiltersOptions, type ModelOrder, type Policy, type WhereOptions } from "@/api/base-api"
|
||||||
|
|
||||||
|
export type FlashcardDeck = {
|
||||||
|
id: number
|
||||||
|
parentDeckId: number | null
|
||||||
|
creatorId: number
|
||||||
|
name: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlashcardDeckWhereOptions = WhereOptions<FlashcardDeck, "creatorId" | "parentDeckId">
|
||||||
|
|
||||||
|
export type FlashcardDeckFiltersOptions = FiltersOptions<{
|
||||||
|
search: string | string[]
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type FlashcardDeckQueryOptions = {
|
||||||
|
where?: FlashcardDeckWhereOptions
|
||||||
|
filters?: FlashcardDeckFiltersOptions
|
||||||
|
order?: ModelOrder[]
|
||||||
|
page?: number
|
||||||
|
perPage?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flashcardDecksApi = {
|
||||||
|
async list(params: FlashcardDeckQueryOptions = {}): Promise<{
|
||||||
|
flashcardDecks: FlashcardDeck[]
|
||||||
|
totalCount: number
|
||||||
|
}> {
|
||||||
|
const { data } = await http.get("/api/flashcard-decks", { params })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async get(flashcardDeckId: number): Promise<{
|
||||||
|
flashcardDeck: FlashcardDeck
|
||||||
|
policy: Policy
|
||||||
|
}> {
|
||||||
|
const { data } = await http.get(`/api/flashcard-decks/${flashcardDeckId}`)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async create(attributes: Partial<FlashcardDeck>): Promise<{
|
||||||
|
flashcardDeck: FlashcardDeck
|
||||||
|
}> {
|
||||||
|
const { data } = await http.post("/api/flashcard-decks", attributes)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async update(
|
||||||
|
flashcardDeckId: number,
|
||||||
|
attributes: Partial<FlashcardDeck>
|
||||||
|
): Promise<{
|
||||||
|
flashcardDeck: FlashcardDeck
|
||||||
|
}> {
|
||||||
|
const { data } = await http.patch(`/api/flashcard-decks/${flashcardDeckId}`, attributes)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async delete(flashcardDeckId: number): Promise<void> {
|
||||||
|
const { data } = await http.delete(`/api/flashcard-decks/${flashcardDeckId}`)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default flashcardDecksApi
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import http from "@/api/http-client"
|
||||||
|
import { type FiltersOptions, type ModelOrder, type Policy, type WhereOptions } from "@/api/base-api"
|
||||||
|
|
||||||
|
export type Flashcard = {
|
||||||
|
id: number
|
||||||
|
flashcardDeckId: number
|
||||||
|
creatorId: number
|
||||||
|
cardType: string
|
||||||
|
front: string
|
||||||
|
back: string | null
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FlashcardWhereOptions = WhereOptions<Flashcard, "flashcardDeckId" | "creatorId" | "cardType">
|
||||||
|
|
||||||
|
export type FlashcardFiltersOptions = FiltersOptions<{
|
||||||
|
search: string | string[]
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type FlashcardQueryOptions = {
|
||||||
|
where?: FlashcardWhereOptions
|
||||||
|
filters?: FlashcardFiltersOptions
|
||||||
|
order?: ModelOrder[]
|
||||||
|
page?: number
|
||||||
|
perPage?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flashcardsApi = {
|
||||||
|
async list(params: FlashcardQueryOptions = {}): Promise<{
|
||||||
|
flashcards: Flashcard[]
|
||||||
|
totalCount: number
|
||||||
|
}> {
|
||||||
|
const { data } = await http.get("/api/flashcards", { params })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async get(flashcardId: number): Promise<{
|
||||||
|
flashcard: Flashcard
|
||||||
|
policy: Policy
|
||||||
|
}> {
|
||||||
|
const { data } = await http.get(`/api/flashcards/${flashcardId}`)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async create(attributes: Partial<Flashcard>): Promise<{
|
||||||
|
flashcard: Flashcard
|
||||||
|
}> {
|
||||||
|
const { data } = await http.post("/api/flashcards", attributes)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async update(
|
||||||
|
flashcardId: number,
|
||||||
|
attributes: Partial<Flashcard>
|
||||||
|
): Promise<{
|
||||||
|
flashcard: Flashcard
|
||||||
|
}> {
|
||||||
|
const { data } = await http.patch(`/api/flashcards/${flashcardId}`, attributes)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async delete(flashcardId: number): Promise<void> {
|
||||||
|
const { data } = await http.delete(`/api/flashcards/${flashcardId}`)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default flashcardsApi
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { type Ref, reactive, toRefs, unref, watch } from "vue"
|
||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import { type Policy } from "@/api/base-api"
|
||||||
|
import flashcardDecksApi, { type FlashcardDeck } from "@/api/flashcard-decks-api"
|
||||||
|
|
||||||
|
export { type FlashcardDeck }
|
||||||
|
|
||||||
|
export function useFlashcardDeck(id: Ref<number | null | undefined>) {
|
||||||
|
const state = reactive<{
|
||||||
|
flashcardDeck: FlashcardDeck | null
|
||||||
|
policy: Policy | null
|
||||||
|
isLoading: boolean
|
||||||
|
isErrored: boolean
|
||||||
|
}>({
|
||||||
|
flashcardDeck: null,
|
||||||
|
policy: null,
|
||||||
|
isLoading: false,
|
||||||
|
isErrored: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch(): Promise<FlashcardDeck> {
|
||||||
|
const staticId = unref(id)
|
||||||
|
if (isNil(staticId)) {
|
||||||
|
throw new Error("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcardDeck, policy } = await flashcardDecksApi.get(staticId)
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcardDeck = flashcardDeck
|
||||||
|
state.policy = policy
|
||||||
|
return flashcardDeck
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch flashcard deck:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(): Promise<FlashcardDeck> {
|
||||||
|
const staticId = unref(id)
|
||||||
|
if (isNil(staticId)) {
|
||||||
|
throw new Error("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(state.flashcardDeck)) {
|
||||||
|
throw new Error("No flashcard deck to save")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcardDeck } = await flashcardDecksApi.update(staticId, state.flashcardDeck)
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcardDeck = flashcardDeck
|
||||||
|
return flashcardDeck
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save flashcard deck:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => unref(id),
|
||||||
|
async (newId) => {
|
||||||
|
if (isNil(newId)) return
|
||||||
|
|
||||||
|
await fetch()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
fetch,
|
||||||
|
refresh: fetch,
|
||||||
|
save,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFlashcardDeck
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { type Ref, reactive, toRefs, ref, unref, watch } from "vue"
|
||||||
|
|
||||||
|
import flashcardDecksApi, {
|
||||||
|
type FlashcardDeck,
|
||||||
|
type FlashcardDeckWhereOptions,
|
||||||
|
type FlashcardDeckFiltersOptions,
|
||||||
|
type FlashcardDeckQueryOptions,
|
||||||
|
} from "@/api/flashcard-decks-api"
|
||||||
|
|
||||||
|
export {
|
||||||
|
type FlashcardDeck,
|
||||||
|
type FlashcardDeckWhereOptions,
|
||||||
|
type FlashcardDeckFiltersOptions,
|
||||||
|
type FlashcardDeckQueryOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFlashcardDecks(
|
||||||
|
queryOptions: Ref<FlashcardDeckQueryOptions> = ref({}),
|
||||||
|
{ skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {}
|
||||||
|
) {
|
||||||
|
const state = reactive<{
|
||||||
|
flashcardDecks: FlashcardDeck[]
|
||||||
|
totalCount: number
|
||||||
|
isLoading: boolean
|
||||||
|
isErrored: boolean
|
||||||
|
}>({
|
||||||
|
flashcardDecks: [],
|
||||||
|
totalCount: 0,
|
||||||
|
isLoading: false,
|
||||||
|
isErrored: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch(): Promise<FlashcardDeck[]> {
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcardDecks, totalCount } = await flashcardDecksApi.list(unref(queryOptions))
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcardDecks = flashcardDecks
|
||||||
|
state.totalCount = totalCount
|
||||||
|
return flashcardDecks
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch flashcard decks:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [skipWatchIf(), unref(queryOptions)],
|
||||||
|
async ([skip]) => {
|
||||||
|
if (skip) return
|
||||||
|
|
||||||
|
await fetch()
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
fetch,
|
||||||
|
refresh: fetch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFlashcardDecks
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { type Ref, reactive, toRefs, unref, watch } from "vue"
|
||||||
|
import { isNil } from "lodash"
|
||||||
|
|
||||||
|
import { type Policy } from "@/api/base-api"
|
||||||
|
import flashcardsApi, { type Flashcard } from "@/api/flashcards-api"
|
||||||
|
|
||||||
|
export { type Flashcard }
|
||||||
|
|
||||||
|
export function useFlashcard(id: Ref<number | null | undefined>) {
|
||||||
|
const state = reactive<{
|
||||||
|
flashcard: Flashcard | null
|
||||||
|
policy: Policy | null
|
||||||
|
isLoading: boolean
|
||||||
|
isErrored: boolean
|
||||||
|
}>({
|
||||||
|
flashcard: null,
|
||||||
|
policy: null,
|
||||||
|
isLoading: false,
|
||||||
|
isErrored: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch(): Promise<Flashcard> {
|
||||||
|
const staticId = unref(id)
|
||||||
|
if (isNil(staticId)) {
|
||||||
|
throw new Error("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcard, policy } = await flashcardsApi.get(staticId)
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcard = flashcard
|
||||||
|
state.policy = policy
|
||||||
|
return flashcard
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch flashcard:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(): Promise<Flashcard> {
|
||||||
|
const staticId = unref(id)
|
||||||
|
if (isNil(staticId)) {
|
||||||
|
throw new Error("id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(state.flashcard)) {
|
||||||
|
throw new Error("No flashcard to save")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcard } = await flashcardsApi.update(staticId, state.flashcard)
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcard = flashcard
|
||||||
|
return flashcard
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save flashcard:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => unref(id),
|
||||||
|
async (newId) => {
|
||||||
|
if (isNil(newId)) return
|
||||||
|
|
||||||
|
await fetch()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
fetch,
|
||||||
|
refresh: fetch,
|
||||||
|
save,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFlashcard
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { type Ref, reactive, toRefs, ref, unref, watch } from "vue"
|
||||||
|
|
||||||
|
import flashcardsApi, {
|
||||||
|
type Flashcard,
|
||||||
|
type FlashcardWhereOptions,
|
||||||
|
type FlashcardFiltersOptions,
|
||||||
|
type FlashcardQueryOptions,
|
||||||
|
} from "@/api/flashcards-api"
|
||||||
|
|
||||||
|
export {
|
||||||
|
type Flashcard,
|
||||||
|
type FlashcardWhereOptions,
|
||||||
|
type FlashcardFiltersOptions,
|
||||||
|
type FlashcardQueryOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFlashcards(
|
||||||
|
queryOptions: Ref<FlashcardQueryOptions> = ref({}),
|
||||||
|
{ skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {}
|
||||||
|
) {
|
||||||
|
const state = reactive<{
|
||||||
|
flashcards: Flashcard[]
|
||||||
|
totalCount: number
|
||||||
|
isLoading: boolean
|
||||||
|
isErrored: boolean
|
||||||
|
}>({
|
||||||
|
flashcards: [],
|
||||||
|
totalCount: 0,
|
||||||
|
isLoading: false,
|
||||||
|
isErrored: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetch(): Promise<Flashcard[]> {
|
||||||
|
state.isLoading = true
|
||||||
|
try {
|
||||||
|
const { flashcards, totalCount } = await flashcardsApi.list(unref(queryOptions))
|
||||||
|
state.isErrored = false
|
||||||
|
state.flashcards = flashcards
|
||||||
|
state.totalCount = totalCount
|
||||||
|
return flashcards
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch flashcards:", error)
|
||||||
|
state.isErrored = true
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [skipWatchIf(), unref(queryOptions)],
|
||||||
|
async ([skip]) => {
|
||||||
|
if (skip) return
|
||||||
|
|
||||||
|
await fetch()
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
fetch,
|
||||||
|
refresh: fetch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFlashcards
|
||||||
Reference in New Issue
Block a user