Adding in flashcard's and decks

This commit is contained in:
2026-06-25 02:39:16 -07:00
parent 78da882bda
commit 535c4c4943
34 changed files with 1248 additions and 1 deletions
@@ -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
+2
View File
@@ -1,3 +1,5 @@
// Controllers
export { CurrentUserController } from "./current-user-controller"
export { FlashcardDecksController } from "./flashcard-decks-controller"
export { FlashcardsController } from "./flashcards-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")
}
+68
View File
@@ -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
+81
View File
@@ -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
+8
View File
@@ -1,16 +1,24 @@
import db from "@/db/db-client"
// Models
import Flashcard from "@/models/flashcard"
import FlashcardDeck from "@/models/flashcard-deck"
import User, { UserRoles } from "@/models/user"
db.addModels([
Flashcard,
FlashcardDeck,
User,
])
// Lazy load scopes
Flashcard.establishScopes()
FlashcardDeck.establishScopes()
User.establishScopes()
export {
Flashcard,
FlashcardDeck,
User,
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
+41
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
// Policy Bundles
export { type BaseScopeOptions } from "./base-policy"
export { FlashcardDecksPolicy } from "./flashcard-decks-policy"
export { FlashcardsPolicy } from "./flashcards-policy"
export { UsersPolicy } from "./users-policy"
+21 -1
View File
@@ -16,7 +16,7 @@ import { logger } from "@/utils/logger"
import { jwtMiddleware, authorizationMiddleware } from "@/middlewares"
import { CurrentUserController, UsersController } from "@/controllers"
import { CurrentUserController, FlashcardDecksController, FlashcardsController, UsersController } from "@/controllers"
export const router = Router()
@@ -42,6 +42,26 @@ router
.patch(UsersController.update)
.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
router.use("/api", (req: Request, res: Response) => {
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
+2
View File
@@ -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
+2
View File
@@ -1,2 +1,4 @@
// Bundled exports
export * as FlashcardDecks from "./flashcard-decks"
export * as Flashcards from "./flashcards"
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
+3
View File
@@ -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
+2
View File
@@ -1 +1,3 @@
export * as FlashcardDecks from "./flashcard-decks"
export * as Flashcards from "./flashcards"
export * as Users from "./users"