feat: add basic data architecture
This commit is contained in:
parent
f959f69de6
commit
ac6e66e0b5
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
EXPO_PUBLIC_SUPABASE_URL='http://127.0.0.1:54321' # Replace `127.0.0.1` with local IP (`hostname -I` on Linux)
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=''
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,6 +31,7 @@ yarn-error.*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
59
README.md
59
README.md
@ -6,9 +6,6 @@ Application mobile en [React Native](https://reactnative.dev/) pour le projet du
|
||||
|
||||
Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours.
|
||||
|
||||
- [Sujet](./docs/SUJET.md)
|
||||
- [Cahier des charges](./docs/CAHIER-DES-CHARGES.md)
|
||||
|
||||
### Membres du Groupe 7
|
||||
|
||||
- [Théo LUDWIG](https://git.unistra.fr/t.ludwig)
|
||||
@ -16,6 +13,22 @@ Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours
|
||||
- [Maxime RUMPLER](https://git.unistra.fr/m.rumpler)
|
||||
- [Maxime RICHARD](https://git.unistra.fr/maximerichard)
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Sujet du projet](./docs/SUJET.md)
|
||||
- [Cahier des charges](./docs/CAHIER-DES-CHARGES.md)
|
||||
- [Modèle Logique des Données (MLD)](./docs/MLD.md)
|
||||
- [Clean Architecture](./docs/ARCHITECTURE.md)
|
||||
- [Conventions développement informatique](./docs/CONVENTIONS.md)
|
||||
|
||||
#### Principaux Outils Informatiques Utilisés
|
||||
|
||||
- [React Native](https://reactnative.dev/) + [Expo](https://expo.io/): Framework pour le développement d'applications mobiles.
|
||||
- [TypeScript](https://www.typescriptlang.org/): Langage de programmation.
|
||||
- [React Native Paper](https://callstack.github.io/react-native-paper/): Bibliothèque de composants pour React Native.
|
||||
<!-- - [WatermelonDB](https://nozbe.github.io/WatermelonDB/): Base de données locale, pour permettre une utilisation hors-ligne de l'application. -->
|
||||
- [Supabase](https://supabase.io/): Backend, serveur d'API pour le stockage des données.
|
||||
|
||||
## Développement du projet en local
|
||||
|
||||
### Prérequis
|
||||
@ -23,6 +36,7 @@ Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours
|
||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||
- [Expo Go](https://expo.io/client)
|
||||
- [Docker](https://www.docker.com/) (facultatif, utilisé pour lancer [Supabase](https://supabase.io/) en local)
|
||||
|
||||
### Installation
|
||||
|
||||
@ -30,6 +44,12 @@ Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours
|
||||
# Cloner le projet
|
||||
git clone git@git.unistra.fr:rrll/p61-project.git
|
||||
|
||||
# Se déplacer dans le répertoire du projet
|
||||
cd p61-project
|
||||
|
||||
# Configurer les variables d'environnement
|
||||
cp .env.example .env
|
||||
|
||||
# Installer les dépendances
|
||||
npm clean-install
|
||||
```
|
||||
@ -40,37 +60,10 @@ npm clean-install
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Linting/Formatting/Tests
|
||||
### Lancer Supabase (facultatif)
|
||||
|
||||
Le code est formaté grâce à [Prettier](https://prettier.io/) et vérifié grâce à [ESLint](https://eslint.org/) et à [TypeScript](https://www.typescriptlang.org/) pour s'assurer que le code respecte les bonnes pratiques de développement, et détecter en amont les possibles erreurs.
|
||||
|
||||
Nous utilisons également [Jest](https://jestjs.io/) pour les tests automatisés.
|
||||
Ce n'est pas strictement nécessaire pour le développement de l'application (même si recommandé), de lancer [Supabase](https://supabase.io/) en local, car l'application est déjà déployée sur un serveur [Supabase](https://supabase.io/) en production.
|
||||
|
||||
```sh
|
||||
# Lint
|
||||
npm run lint:prettier
|
||||
npm run lint:eslint
|
||||
npm run lint:typescript
|
||||
|
||||
# Test
|
||||
npm run test
|
||||
npm run supabase
|
||||
```
|
||||
|
||||
Une pipeline CI ([`.gitlab-ci.yml`](.gitlab-ci.yml)) est en place pour vérifier que le code respecte ces bonnes pratiques et que les tests passent.
|
||||
|
||||
### GitFlow
|
||||
|
||||
Le projet suit la convention [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) reposant sur 2 branches principales:
|
||||
|
||||
- `main` (ou `master`): Contient le code de la dernière version stable et déployé en production.
|
||||
- `develop`: Contient le code en cours de développement. Les nouvelles fonctionnalités et les correctifs de bugs sont fusionnés ici.
|
||||
|
||||
### Convention des commits
|
||||
|
||||
Les commits respectent la convention [Conventional Commits](https://www.conventionalcommits.org/) et [Semantic Versioning](https://semver.org/) pour la gestion des versions et des releases en fonction des commits.
|
||||
|
||||
Les commits doivent être **atomiques** c'est à dire qu'il respecte 3 règles:
|
||||
|
||||
- Ne concerne qu'un seul sujet (une fonctionnalité, une correction de bug, etc.)
|
||||
- Doit avoir un message clair et concis
|
||||
- Ne doit pas rendre de dépôt "incohérent" (bloque la CI du projet)
|
||||
|
2
app.json
2
app.json
@ -5,7 +5,7 @@
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"scheme": "p61-project",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"splash": {
|
||||
"image": "./assets/images/splashscreen.jpg",
|
||||
|
@ -1,170 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<HomePage /> renders correctly 1`] = `
|
||||
<RNCSafeAreaView
|
||||
edges={
|
||||
{
|
||||
"bottom": "additive",
|
||||
"left": "additive",
|
||||
"right": "additive",
|
||||
"top": "additive",
|
||||
}
|
||||
}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
style={
|
||||
{
|
||||
"backgroundColor": "rgba(103, 80, 164, 1)",
|
||||
"borderRadius": 20,
|
||||
"shadowColor": "#000",
|
||||
"shadowOffset": {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0,
|
||||
"shadowRadius": 0,
|
||||
}
|
||||
}
|
||||
testID="button-container-outer-layer"
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
style={
|
||||
{
|
||||
"backgroundColor": "rgba(103, 80, 164, 1)",
|
||||
"borderColor": "transparent",
|
||||
"borderRadius": 20,
|
||||
"borderStyle": "solid",
|
||||
"borderWidth": 0,
|
||||
"flex": undefined,
|
||||
"minWidth": 64,
|
||||
"shadowColor": "#000",
|
||||
"shadowOffset": {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0,
|
||||
"shadowRadius": 0,
|
||||
}
|
||||
}
|
||||
testID="button-container"
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": false,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
[
|
||||
{
|
||||
"overflow": "hidden",
|
||||
},
|
||||
{
|
||||
"borderRadius": 20,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="button"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
selectable={false}
|
||||
style={
|
||||
[
|
||||
{
|
||||
"textAlign": "left",
|
||||
},
|
||||
{
|
||||
"color": "rgba(28, 27, 31, 1)",
|
||||
"writingDirection": "ltr",
|
||||
},
|
||||
[
|
||||
{
|
||||
"fontFamily": "System",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "500",
|
||||
"letterSpacing": 0.1,
|
||||
"lineHeight": 20,
|
||||
},
|
||||
[
|
||||
{
|
||||
"marginHorizontal": 16,
|
||||
"marginVertical": 9,
|
||||
"textAlign": "center",
|
||||
},
|
||||
false,
|
||||
{
|
||||
"marginHorizontal": 24,
|
||||
"marginVertical": 10,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
{
|
||||
"color": "rgba(255, 255, 255, 1)",
|
||||
"fontFamily": "System",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "500",
|
||||
"letterSpacing": 0.1,
|
||||
"lineHeight": 20,
|
||||
},
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
]
|
||||
}
|
||||
testID="button-text"
|
||||
>
|
||||
Press me
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
@ -1,14 +0,0 @@
|
||||
import renderer from "react-test-renderer"
|
||||
|
||||
import HomePage from "@/app/(pages)/index"
|
||||
|
||||
describe("<HomePage />", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders correctly", () => {
|
||||
const tree = renderer.create(<HomePage />).toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,18 +1,24 @@
|
||||
import { StyleSheet } from "react-native"
|
||||
import { Button } from "react-native-paper"
|
||||
import { StyleSheet, Text, View } from "react-native"
|
||||
import { SafeAreaView } from "react-native-safe-area-context"
|
||||
|
||||
import { useHabitsTracker } from "@/contexts/HabitsTracker"
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const { habitsTrackerPresenterState } = useHabitsTracker()
|
||||
const { habitsTracker } = habitsTrackerPresenterState
|
||||
const { habitProgressHistories } = habitsTracker
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
return console.log("Pressed")
|
||||
}}
|
||||
>
|
||||
Press me
|
||||
</Button>
|
||||
{habitProgressHistories.map((progressHistory) => {
|
||||
const { habit } = progressHistory
|
||||
|
||||
return (
|
||||
<View key={habit.id}>
|
||||
<Text>{habit.name}</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { StatusBar } from "expo-status-bar"
|
||||
import CanterburyFont from "../assets/fonts/Canterbury.ttf"
|
||||
import GeoramFont from "../assets/fonts/Georama-Black.ttf"
|
||||
import SpaceMonoFont from "../assets/fonts/SpaceMono-Regular.ttf"
|
||||
import { HabitsTrackerProvider } from "@/contexts/HabitsTracker"
|
||||
|
||||
export { ErrorBoundary } from "expo-router"
|
||||
|
||||
@ -48,26 +49,28 @@ const RootLayout: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<PaperProvider
|
||||
theme={{
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: "#f57c00",
|
||||
secondary: "#fbc02d",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
<HabitsTrackerProvider>
|
||||
<PaperProvider
|
||||
theme={{
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: "#f57c00",
|
||||
secondary: "#fbc02d",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(pages)" />
|
||||
</Stack>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(pages)" />
|
||||
</Stack>
|
||||
|
||||
<StatusBar style="dark" />
|
||||
</PaperProvider>
|
||||
<StatusBar style="dark" />
|
||||
</PaperProvider>
|
||||
</HabitsTrackerProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
54
contexts/HabitsTracker.tsx
Normal file
54
contexts/HabitsTracker.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { createContext, useContext, useEffect } from "react"
|
||||
|
||||
import type {
|
||||
HabitsTrackerPresenter,
|
||||
HabitsTrackerPresenterState,
|
||||
} from "@/data/infrastructure/presenters/HabitsTrackerPresenter"
|
||||
import { usePresenterState } from "@/hooks/usePresenterState"
|
||||
import { habitsTrackerPresenter } from "@/data/infrastructure"
|
||||
|
||||
export interface HabitsTrackerContextValue {
|
||||
habitsTrackerPresenterState: HabitsTrackerPresenterState
|
||||
habitsTrackerPresenter: HabitsTrackerPresenter
|
||||
}
|
||||
|
||||
const defaultHabitsTrackerContextValue = {} as HabitsTrackerContextValue
|
||||
const HabitsTrackerContext = createContext<HabitsTrackerContextValue>(
|
||||
defaultHabitsTrackerContextValue,
|
||||
)
|
||||
|
||||
interface HabitsTrackerProviderProps extends React.PropsWithChildren {}
|
||||
|
||||
export const HabitsTrackerProvider: React.FC<HabitsTrackerProviderProps> = (
|
||||
props,
|
||||
) => {
|
||||
const { children } = props
|
||||
|
||||
useEffect(() => {
|
||||
habitsTrackerPresenter
|
||||
.retrieveHabitsTracker({ userId: "1" })
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const habitsTrackerPresenterState = usePresenterState(habitsTrackerPresenter)
|
||||
|
||||
return (
|
||||
<HabitsTrackerContext.Provider
|
||||
value={{ habitsTrackerPresenterState, habitsTrackerPresenter }}
|
||||
>
|
||||
{children}
|
||||
</HabitsTrackerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHabitsTracker = (): HabitsTrackerContextValue => {
|
||||
const context = useContext(HabitsTrackerContext)
|
||||
if (context === defaultHabitsTrackerContextValue) {
|
||||
throw new Error(
|
||||
"`useHabitsTracker` must be used within a `HabitsTrackerProvider`.",
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
108
data/domain/entities/Goal.ts
Normal file
108
data/domain/entities/Goal.ts
Normal file
@ -0,0 +1,108 @@
|
||||
export const GOAL_FREQUENCIES = ["daily", "weekly", "monthly"] as const
|
||||
export type GoalFrequency = (typeof GOAL_FREQUENCIES)[number]
|
||||
|
||||
export const GOAL_TYPES = ["numeric", "boolean"] as const
|
||||
export type GoalType = (typeof GOAL_TYPES)[number]
|
||||
|
||||
interface GoalBase {
|
||||
frequency: GoalFrequency
|
||||
}
|
||||
export abstract class Goal implements GoalBase {
|
||||
public frequency: GoalBase["frequency"]
|
||||
public abstract readonly type: GoalType
|
||||
|
||||
public constructor(options: GoalBase) {
|
||||
const { frequency } = options
|
||||
this.frequency = frequency
|
||||
}
|
||||
|
||||
public static isNumeric(goal: Goal): goal is GoalNumeric {
|
||||
return goal.type === "numeric"
|
||||
}
|
||||
public isNumeric(): this is GoalNumeric {
|
||||
return Goal.isNumeric(this)
|
||||
}
|
||||
|
||||
public static isBoolean(goal: Goal): goal is GoalBoolean {
|
||||
return goal.type === "boolean"
|
||||
}
|
||||
public isBoolean(): this is GoalBoolean {
|
||||
return Goal.isBoolean(this)
|
||||
}
|
||||
}
|
||||
|
||||
interface GoalProgressBase {
|
||||
goal: Goal
|
||||
}
|
||||
export abstract class GoalProgress implements GoalProgressBase {
|
||||
public abstract readonly goal: Goal
|
||||
public abstract isCompleted(): boolean
|
||||
}
|
||||
|
||||
interface GoalNumericOptions extends GoalBase {
|
||||
target: {
|
||||
value: number
|
||||
unit: string
|
||||
}
|
||||
}
|
||||
export class GoalNumeric extends Goal {
|
||||
public readonly type = "numeric"
|
||||
public target: {
|
||||
value: number
|
||||
unit: string
|
||||
}
|
||||
|
||||
public constructor(options: GoalNumericOptions) {
|
||||
super(options)
|
||||
const { target } = options
|
||||
this.target = target
|
||||
}
|
||||
}
|
||||
interface GoalNumericProgressOptions extends GoalProgressBase {
|
||||
goal: GoalNumeric
|
||||
progress: number
|
||||
}
|
||||
export class GoalNumericProgress extends GoalProgress {
|
||||
public readonly goal: GoalNumeric
|
||||
public readonly progress: number
|
||||
|
||||
public constructor(options: GoalNumericProgressOptions) {
|
||||
const { goal, progress } = options
|
||||
super()
|
||||
this.goal = goal
|
||||
this.progress = progress
|
||||
}
|
||||
|
||||
public override isCompleted(): boolean {
|
||||
return this.progress >= this.goal.target.value
|
||||
}
|
||||
|
||||
public progressRatio(): number {
|
||||
return this.goal.target.value <= 0
|
||||
? 0
|
||||
: this.progress / this.goal.target.value
|
||||
}
|
||||
}
|
||||
|
||||
export class GoalBoolean extends Goal {
|
||||
public readonly type = "boolean"
|
||||
}
|
||||
interface GoalBooleanProgressOptions extends GoalProgressBase {
|
||||
goal: GoalBoolean
|
||||
progress: boolean
|
||||
}
|
||||
export class GoalBooleanProgress extends GoalProgress {
|
||||
public readonly goal: GoalBoolean
|
||||
public progress: boolean
|
||||
|
||||
public constructor(options: GoalBooleanProgressOptions) {
|
||||
const { goal, progress } = options
|
||||
super()
|
||||
this.goal = goal
|
||||
this.progress = progress
|
||||
}
|
||||
|
||||
public override isCompleted(): boolean {
|
||||
return this.progress
|
||||
}
|
||||
}
|
36
data/domain/entities/Habit.ts
Normal file
36
data/domain/entities/Habit.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { Goal } from "./Goal"
|
||||
import type { User } from "./User"
|
||||
import type { EntityOptions } from "./_Entity"
|
||||
import { Entity } from "./_Entity"
|
||||
|
||||
export interface HabitOptions extends EntityOptions {
|
||||
userId: User["id"]
|
||||
name: string
|
||||
color: string
|
||||
icon: string
|
||||
goal: Goal
|
||||
startDate: Date
|
||||
endDate?: Date
|
||||
}
|
||||
|
||||
export class Habit extends Entity implements HabitOptions {
|
||||
public userId: HabitOptions["userId"]
|
||||
public name: HabitOptions["name"]
|
||||
public color: HabitOptions["color"]
|
||||
public icon: HabitOptions["icon"]
|
||||
public goal: HabitOptions["goal"]
|
||||
public startDate: HabitOptions["startDate"]
|
||||
public endDate?: HabitOptions["endDate"]
|
||||
|
||||
public constructor(options: HabitOptions) {
|
||||
const { id, userId, name, color, icon, goal, startDate, endDate } = options
|
||||
super({ id })
|
||||
this.userId = userId
|
||||
this.name = name
|
||||
this.color = color
|
||||
this.icon = icon
|
||||
this.goal = goal
|
||||
this.startDate = startDate
|
||||
this.endDate = endDate
|
||||
}
|
||||
}
|
24
data/domain/entities/HabitProgress.ts
Normal file
24
data/domain/entities/HabitProgress.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { GoalProgress } from "./Goal"
|
||||
import type { Habit } from "./Habit"
|
||||
import type { EntityOptions } from "./_Entity"
|
||||
import { Entity } from "./_Entity"
|
||||
|
||||
export interface HabitProgressOptions extends EntityOptions {
|
||||
habitId: Habit["id"]
|
||||
goalProgress: GoalProgress
|
||||
date: Date
|
||||
}
|
||||
|
||||
export class HabitProgress extends Entity implements HabitProgressOptions {
|
||||
public habitId: HabitProgressOptions["habitId"]
|
||||
public goalProgress: HabitProgressOptions["goalProgress"]
|
||||
public date: HabitProgressOptions["date"]
|
||||
|
||||
public constructor(options: HabitProgressOptions) {
|
||||
const { id, habitId, goalProgress, date } = options
|
||||
super({ id })
|
||||
this.habitId = habitId
|
||||
this.goalProgress = goalProgress
|
||||
this.date = date
|
||||
}
|
||||
}
|
18
data/domain/entities/HabitProgressHistory.ts
Normal file
18
data/domain/entities/HabitProgressHistory.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Habit } from "./Habit"
|
||||
import type { HabitProgress } from "./HabitProgress"
|
||||
|
||||
export interface HabitProgressHistoryOptions {
|
||||
habit: Habit
|
||||
habitProgresses: HabitProgress[]
|
||||
}
|
||||
|
||||
export class HabitProgressHistory implements HabitProgressHistoryOptions {
|
||||
public habit: Habit
|
||||
public habitProgresses: HabitProgress[]
|
||||
|
||||
public constructor(options: HabitProgressHistoryOptions) {
|
||||
const { habit, habitProgresses } = options
|
||||
this.habit = habit
|
||||
this.habitProgresses = habitProgresses
|
||||
}
|
||||
}
|
18
data/domain/entities/HabitsTracker.ts
Normal file
18
data/domain/entities/HabitsTracker.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { HabitProgressHistory } from "./HabitProgressHistory"
|
||||
|
||||
export interface HabitsTrackerOptions {
|
||||
habitProgressHistories: HabitProgressHistory[]
|
||||
}
|
||||
|
||||
export class HabitsTracker implements HabitsTrackerOptions {
|
||||
public habitProgressHistories: HabitProgressHistory[]
|
||||
|
||||
public constructor(options: HabitsTrackerOptions) {
|
||||
const { habitProgressHistories } = options
|
||||
this.habitProgressHistories = habitProgressHistories
|
||||
}
|
||||
|
||||
public static default(): HabitsTracker {
|
||||
return new HabitsTracker({ habitProgressHistories: [] })
|
||||
}
|
||||
}
|
19
data/domain/entities/User.ts
Normal file
19
data/domain/entities/User.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { EntityOptions } from "./_Entity"
|
||||
import { Entity } from "./_Entity"
|
||||
|
||||
export interface UserOptions extends EntityOptions {
|
||||
email: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export class User extends Entity implements UserOptions {
|
||||
public email: UserOptions["email"]
|
||||
public displayName: UserOptions["displayName"]
|
||||
|
||||
public constructor(options: UserOptions) {
|
||||
const { id, email, displayName } = options
|
||||
super({ id })
|
||||
this.email = email
|
||||
this.displayName = displayName
|
||||
}
|
||||
}
|
16
data/domain/entities/_Entity.ts
Normal file
16
data/domain/entities/_Entity.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface EntityOptions {
|
||||
id: string
|
||||
}
|
||||
|
||||
export abstract class Entity implements EntityOptions {
|
||||
public readonly id: string
|
||||
|
||||
public constructor(options: EntityOptions) {
|
||||
const { id } = options
|
||||
this.id = id
|
||||
}
|
||||
|
||||
// public equals(entity: Entity): boolean {
|
||||
// return entity.id === this.id
|
||||
// }
|
||||
}
|
10
data/domain/repositories/GetHabitProgresses.ts
Normal file
10
data/domain/repositories/GetHabitProgresses.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { Habit } from "../entities/Habit"
|
||||
import type { HabitProgress } from "../entities/HabitProgress"
|
||||
|
||||
export interface GetHabitProgressesOptions {
|
||||
habit: Habit
|
||||
}
|
||||
|
||||
export interface GetHabitProgressesRepository {
|
||||
execute: (options: GetHabitProgressesOptions) => Promise<HabitProgress[]>
|
||||
}
|
10
data/domain/repositories/GetHabitsByUserId.ts
Normal file
10
data/domain/repositories/GetHabitsByUserId.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { Habit } from "../entities/Habit"
|
||||
import type { User } from "../entities/User"
|
||||
|
||||
export interface GetHabitsByUserIdOptions {
|
||||
userId: User["id"]
|
||||
}
|
||||
|
||||
export interface GetHabitsByUserIdRepository {
|
||||
execute: (options: GetHabitsByUserIdOptions) => Promise<Habit[]>
|
||||
}
|
45
data/domain/use-cases/RetrieveHabitsTracker.ts
Normal file
45
data/domain/use-cases/RetrieveHabitsTracker.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { HabitProgressHistory } from "../entities/HabitProgressHistory"
|
||||
import { HabitsTracker } from "../entities/HabitsTracker"
|
||||
import type { User } from "../entities/User"
|
||||
import type { GetHabitProgressesRepository } from "../repositories/GetHabitProgresses"
|
||||
import type { GetHabitsByUserIdRepository } from "../repositories/GetHabitsByUserId"
|
||||
|
||||
export interface RetrieveHabitsTrackerUseCaseDependencyOptions {
|
||||
getHabitsByUserIdRepository: GetHabitsByUserIdRepository
|
||||
getHabitProgressesRepository: GetHabitProgressesRepository
|
||||
}
|
||||
|
||||
export interface RetrieveHabitsTrackerUseCaseOptions {
|
||||
userId: User["id"]
|
||||
}
|
||||
|
||||
export class RetrieveHabitsTrackerUseCase
|
||||
implements RetrieveHabitsTrackerUseCaseDependencyOptions
|
||||
{
|
||||
public getHabitsByUserIdRepository: GetHabitsByUserIdRepository
|
||||
public getHabitProgressesRepository: GetHabitProgressesRepository
|
||||
|
||||
public constructor(options: RetrieveHabitsTrackerUseCaseDependencyOptions) {
|
||||
this.getHabitsByUserIdRepository = options.getHabitsByUserIdRepository
|
||||
this.getHabitProgressesRepository = options.getHabitProgressesRepository
|
||||
}
|
||||
|
||||
public async execute(
|
||||
options: RetrieveHabitsTrackerUseCaseOptions,
|
||||
): Promise<HabitsTracker> {
|
||||
const { userId } = options
|
||||
const habits = await this.getHabitsByUserIdRepository.execute({ userId })
|
||||
const habitProgressHistories = await Promise.all(
|
||||
habits.map(async (habit) => {
|
||||
const habitProgresses = await this.getHabitProgressesRepository.execute(
|
||||
{
|
||||
habit,
|
||||
},
|
||||
)
|
||||
return new HabitProgressHistory({ habit, habitProgresses })
|
||||
}),
|
||||
)
|
||||
const habitsTracker = new HabitsTracker({ habitProgressHistories })
|
||||
return habitsTracker
|
||||
}
|
||||
}
|
34
data/infrastructure/index.ts
Normal file
34
data/infrastructure/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// export const taskRepository = new TaskLocalStorageRepository()
|
||||
// export const taskService = new TaskService(taskRepository)
|
||||
// export const taskPresenter = new TaskPresenter(taskService)
|
||||
|
||||
import { RetrieveHabitsTrackerUseCase } from "../domain/use-cases/RetrieveHabitsTracker"
|
||||
import { HabitsTrackerPresenter } from "./presenters/HabitsTrackerPresenter"
|
||||
import { GetHabitProgressesSupabaseRepository } from "./repositories/supabase/lib/GetHabitProgresses"
|
||||
import { GetHabitsByUserIdSupabaseRepository } from "./repositories/supabase/lib/GetHabitsByUserId"
|
||||
import { supabaseClient } from "./repositories/supabase/supabase"
|
||||
|
||||
/**
|
||||
* Repositories
|
||||
*/
|
||||
const getHabitProgressesRepository = new GetHabitProgressesSupabaseRepository({
|
||||
supabaseClient,
|
||||
})
|
||||
const getHabitsByUserIdRepository = new GetHabitsByUserIdSupabaseRepository({
|
||||
supabaseClient,
|
||||
})
|
||||
|
||||
/**
|
||||
* Use Cases
|
||||
*/
|
||||
const retrieveHabitsTrackerUseCase = new RetrieveHabitsTrackerUseCase({
|
||||
getHabitProgressesRepository,
|
||||
getHabitsByUserIdRepository,
|
||||
})
|
||||
|
||||
/**
|
||||
* Presenters
|
||||
*/
|
||||
export const habitsTrackerPresenter = new HabitsTrackerPresenter({
|
||||
retrieveHabitsTrackerUseCase,
|
||||
})
|
38
data/infrastructure/presenters/HabitsTrackerPresenter.ts
Normal file
38
data/infrastructure/presenters/HabitsTrackerPresenter.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { HabitsTracker } from "@/data/domain/entities/HabitsTracker"
|
||||
import { Presenter } from "./_Presenter"
|
||||
import type {
|
||||
RetrieveHabitsTrackerUseCase,
|
||||
RetrieveHabitsTrackerUseCaseOptions,
|
||||
} from "@/data/domain/use-cases/RetrieveHabitsTracker"
|
||||
|
||||
export interface HabitsTrackerPresenterState {
|
||||
habitsTracker: HabitsTracker
|
||||
}
|
||||
|
||||
export interface HabitsTrackerPresenterOptions {
|
||||
retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||
}
|
||||
|
||||
export class HabitsTrackerPresenter
|
||||
extends Presenter<HabitsTrackerPresenterState>
|
||||
implements HabitsTrackerPresenterOptions
|
||||
{
|
||||
public retrieveHabitsTrackerUseCase: RetrieveHabitsTrackerUseCase
|
||||
|
||||
public constructor(options: HabitsTrackerPresenterOptions) {
|
||||
const { retrieveHabitsTrackerUseCase } = options
|
||||
const habitsTracker = HabitsTracker.default()
|
||||
super({ habitsTracker })
|
||||
this.retrieveHabitsTrackerUseCase = retrieveHabitsTrackerUseCase
|
||||
}
|
||||
|
||||
public async retrieveHabitsTracker(
|
||||
options: RetrieveHabitsTrackerUseCaseOptions,
|
||||
): Promise<void> {
|
||||
const habitsTracker =
|
||||
await this.retrieveHabitsTrackerUseCase.execute(options)
|
||||
this.setState((state) => {
|
||||
state.habitsTracker = habitsTracker
|
||||
})
|
||||
}
|
||||
}
|
50
data/infrastructure/presenters/_Presenter.ts
Normal file
50
data/infrastructure/presenters/_Presenter.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { produce } from "immer"
|
||||
|
||||
type Listener<S> = (state: S) => void
|
||||
|
||||
type SetStateAction<S> = (state: S) => void
|
||||
|
||||
export abstract class Presenter<S> {
|
||||
private _state: S
|
||||
private readonly _initialState: S
|
||||
private readonly _listeners: Array<Listener<S>>
|
||||
|
||||
public constructor(initialState: S) {
|
||||
this._state = initialState
|
||||
this._initialState = initialState
|
||||
this._listeners = []
|
||||
}
|
||||
|
||||
public get initialState(): S {
|
||||
return this._initialState
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Set the state of the presenter.
|
||||
* @param action A function that receives the current state and can update it by mutating it.
|
||||
* @returns The new state.
|
||||
*/
|
||||
public setState(action: SetStateAction<S>): S {
|
||||
this._state = produce(this._state, action)
|
||||
this.notifyListeners()
|
||||
return this._state
|
||||
}
|
||||
|
||||
public subscribe(listener: Listener<S>): void {
|
||||
this._listeners.push(listener)
|
||||
}
|
||||
|
||||
public unsubscribe(listener: Listener<S>): void {
|
||||
const listenerIndex = this._listeners.indexOf(listener)
|
||||
const listenerFound = listenerIndex !== -1
|
||||
if (listenerFound) {
|
||||
this._listeners.splice(listenerIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
private notifyListeners(): void {
|
||||
for (const listener of this._listeners) {
|
||||
listener(this._state)
|
||||
}
|
||||
}
|
||||
}
|
4
data/infrastructure/repositories/supabase/.gitignore
vendored
Normal file
4
data/infrastructure/repositories/supabase/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
161
data/infrastructure/repositories/supabase/config.toml
Normal file
161
data/infrastructure/repositories/supabase/config.toml
Normal file
@ -0,0 +1,161 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "p61-project"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. public and storage are always included.
|
||||
schemas = ["public", "storage", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request. public is always included.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 15
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = true
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
# Template for sending OTP to users
|
||||
template = "Your code is {{ .Code }} ."
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
[auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
[auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
|
||||
[analytics]
|
||||
enabled = false
|
||||
port = 54327
|
||||
vector_port = 54328
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
@ -0,0 +1,49 @@
|
||||
import type { GetHabitProgressesRepository } from "@/data/domain/repositories/GetHabitProgresses"
|
||||
import { SupabaseRepository } from "./_SupabaseRepository"
|
||||
import { HabitProgress } from "@/data/domain/entities/HabitProgress"
|
||||
import type { GoalProgress } from "@/data/domain/entities/Goal"
|
||||
import {
|
||||
GoalBooleanProgress,
|
||||
GoalNumericProgress,
|
||||
} from "@/data/domain/entities/Goal"
|
||||
|
||||
export class GetHabitProgressesSupabaseRepository
|
||||
extends SupabaseRepository
|
||||
implements GetHabitProgressesRepository
|
||||
{
|
||||
execute: GetHabitProgressesRepository["execute"] = async (options) => {
|
||||
const { habit } = options
|
||||
const { data, error } = await this.supabaseClient
|
||||
.from("habits_progresses")
|
||||
.select("*")
|
||||
.eq("habit_id", habit.id)
|
||||
if (error != null) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
const habitProgresses = data.map((item) => {
|
||||
let goalProgress: GoalProgress | null = null
|
||||
if (habit.goal.isNumeric()) {
|
||||
goalProgress = new GoalNumericProgress({
|
||||
goal: habit.goal,
|
||||
progress: item.goal_progress,
|
||||
})
|
||||
} else if (habit.goal.isBoolean()) {
|
||||
goalProgress = new GoalBooleanProgress({
|
||||
goal: habit.goal,
|
||||
progress: item.goal_progress === 1,
|
||||
})
|
||||
}
|
||||
if (goalProgress == null) {
|
||||
throw new Error("Goal progress is null.")
|
||||
}
|
||||
const habitProgress = new HabitProgress({
|
||||
id: item.id.toString(),
|
||||
habitId: item.habit_id.toString(),
|
||||
goalProgress,
|
||||
date: new Date(item.date),
|
||||
})
|
||||
return habitProgress
|
||||
})
|
||||
return habitProgresses
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import type { GetHabitsByUserIdRepository } from "@/data/domain/repositories/GetHabitsByUserId"
|
||||
import { SupabaseRepository } from "./_SupabaseRepository"
|
||||
import { Habit } from "@/data/domain/entities/Habit"
|
||||
import type { Goal } from "@/data/domain/entities/Goal"
|
||||
import { GoalBoolean, GoalNumeric } from "@/data/domain/entities/Goal"
|
||||
|
||||
export class GetHabitsByUserIdSupabaseRepository
|
||||
extends SupabaseRepository
|
||||
implements GetHabitsByUserIdRepository
|
||||
{
|
||||
// execute: GetHabitsByUserIdRepository["execute"] = async (options) => {
|
||||
// const { userId } = options
|
||||
// const { data, error } = await this.supabaseClient
|
||||
// .from("habits")
|
||||
// .select("*")
|
||||
// .eq("user_id", userId)
|
||||
execute: GetHabitsByUserIdRepository["execute"] = async () => {
|
||||
const { data, error } = await this.supabaseClient.from("habits").select("*")
|
||||
if (error != null) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
return data.map((item) => {
|
||||
let goal: Goal
|
||||
if (item.goal_target != null && item.goal_target_unit != null) {
|
||||
goal = new GoalNumeric({
|
||||
frequency: item.goal_frequency,
|
||||
target: {
|
||||
value: item.goal_target,
|
||||
unit: item.goal_target_unit,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
goal = new GoalBoolean({
|
||||
frequency: item.goal_frequency,
|
||||
})
|
||||
}
|
||||
const habit = new Habit({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
color: item.color,
|
||||
icon: item.icon,
|
||||
userId: item.user_id.toString(),
|
||||
startDate: new Date(item.start_date),
|
||||
endDate: item.end_date != null ? new Date(item.end_date) : undefined,
|
||||
goal,
|
||||
})
|
||||
return habit
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import type { SupabaseClient } from "@supabase/supabase-js"
|
||||
|
||||
import type { Database } from "../supabase-types"
|
||||
|
||||
export interface SupabaseRepositoryOptions {
|
||||
supabaseClient: SupabaseClient<Database>
|
||||
}
|
||||
|
||||
export abstract class SupabaseRepository implements SupabaseRepositoryOptions {
|
||||
public supabaseClient: SupabaseRepositoryOptions["supabaseClient"]
|
||||
|
||||
public constructor(options: SupabaseRepositoryOptions) {
|
||||
this.supabaseClient = options.supabaseClient
|
||||
}
|
||||
}
|
@ -0,0 +1,304 @@
|
||||
CREATE TYPE "public"."goal_frequency" AS enum ('daily', 'weekly', 'monthly');
|
||||
|
||||
CREATE TABLE "public"."habits" (
|
||||
"id" bigint generated by DEFAULT AS identity NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"color" text NOT NULL,
|
||||
"icon" text NOT NULL,
|
||||
"start_date" timestamp WITH time zone NOT NULL DEFAULT NOW(),
|
||||
"end_date" timestamp WITH time zone,
|
||||
"goal_frequency" goal_frequency NOT NULL,
|
||||
"goal_target" bigint,
|
||||
"goal_target_unit" text,
|
||||
"user_id" uuid NOT NULL DEFAULT gen_random_uuid()
|
||||
);
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits" enable ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE "public"."habits_progresses" (
|
||||
"id" bigint generated by DEFAULT AS identity NOT NULL,
|
||||
"habit_id" bigint NOT NULL,
|
||||
"date" timestamp WITH time zone NOT NULL DEFAULT NOW(),
|
||||
"goal_progress" bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits_progresses" enable ROW LEVEL SECURITY;
|
||||
|
||||
CREATE UNIQUE INDEX habits_id_key ON public.habits USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX habits_pkey ON public.habits USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX habits_progresses_id_key ON public.habits_progresses USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX habits_progresses_pkey ON public.habits_progresses USING btree (id);
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits"
|
||||
ADD
|
||||
CONSTRAINT "habits_pkey" PRIMARY KEY USING INDEX "habits_pkey";
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits_progresses"
|
||||
ADD
|
||||
CONSTRAINT "habits_progresses_pkey" PRIMARY KEY USING INDEX "habits_progresses_pkey";
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits"
|
||||
ADD
|
||||
CONSTRAINT "habits_id_key" UNIQUE USING INDEX "habits_id_key";
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits"
|
||||
ADD
|
||||
CONSTRAINT "public_habits_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE NOT valid;
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits" validate CONSTRAINT "public_habits_user_id_fkey";
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits_progresses"
|
||||
ADD
|
||||
CONSTRAINT "habits_progresses_id_key" UNIQUE USING INDEX "habits_progresses_id_key";
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits_progresses"
|
||||
ADD
|
||||
CONSTRAINT "public_habits_progresses_habit_id_fkey" FOREIGN KEY (habit_id) REFERENCES habits(id) ON UPDATE CASCADE ON DELETE CASCADE NOT valid;
|
||||
|
||||
ALTER TABLE
|
||||
"public"."habits_progresses" validate CONSTRAINT "public_habits_progresses_habit_id_fkey";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits" TO "anon";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits" TO "authenticated";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits" TO "service_role";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits_progresses" TO "anon";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits_progresses" TO "authenticated";
|
||||
|
||||
GRANT DELETE ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT
|
||||
INSERT
|
||||
ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT REFERENCES ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT trigger ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT TRUNCATE ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
GRANT
|
||||
UPDATE
|
||||
ON TABLE "public"."habits_progresses" TO "service_role";
|
||||
|
||||
CREATE policy "Enable delete for users based on user_id" ON "public"."habits" AS permissive FOR DELETE TO public USING ((auth.uid() = user_id));
|
||||
|
||||
CREATE policy "Enable insert for users based on user_id" ON "public"."habits" AS permissive FOR
|
||||
INSERT
|
||||
TO public WITH CHECK ((auth.uid() = user_id));
|
||||
|
||||
CREATE policy "Enable select for users based on user_id" ON "public"."habits" AS permissive FOR
|
||||
SELECT
|
||||
TO public USING ((auth.uid() = user_id));
|
||||
|
||||
CREATE policy "Enable update for users based on user_id" ON "public"."habits" AS permissive FOR
|
||||
UPDATE
|
||||
TO public USING ((auth.uid() = user_id)) WITH CHECK ((auth.uid() = user_id));
|
||||
|
||||
CREATE policy "Enable delete for users based on user_id" ON "public"."habits_progresses" AS permissive FOR DELETE TO public USING (
|
||||
(
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
(
|
||||
habits_progresses habit_progress
|
||||
JOIN habits habit ON ((habit_progress.habit_id = habit.id))
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(habit_progress.id = habits_progresses.id)
|
||||
AND (habit.user_id = auth.uid())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE policy "Enable insert for users based on user_id" ON "public"."habits_progresses" AS permissive FOR
|
||||
INSERT
|
||||
TO public WITH CHECK (
|
||||
(
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
(
|
||||
habits_progresses habit_progress
|
||||
JOIN habits habit ON ((habit_progress.habit_id = habit.id))
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(habit_progress.id = habits_progresses.id)
|
||||
AND (habit.user_id = auth.uid())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE policy "Enable select for users based on user_id" ON "public"."habits_progresses" AS permissive FOR
|
||||
SELECT
|
||||
TO public USING (
|
||||
(
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
(
|
||||
habits_progresses habit_progress
|
||||
JOIN habits habit ON ((habit_progress.habit_id = habit.id))
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(habit_progress.id = habits_progresses.id)
|
||||
AND (habit.user_id = auth.uid())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE policy "Enable update for users based on user_id" ON "public"."habits_progresses" AS permissive FOR
|
||||
UPDATE
|
||||
TO public USING (
|
||||
(
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
(
|
||||
habits_progresses habit_progress
|
||||
JOIN habits habit ON ((habit_progress.habit_id = habit.id))
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(habit_progress.id = habits_progresses.id)
|
||||
AND (habit.user_id = auth.uid())
|
||||
)
|
||||
)
|
||||
)
|
||||
) WITH CHECK (
|
||||
(
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
(
|
||||
habits_progresses habit_progress
|
||||
JOIN habits habit ON ((habit_progress.habit_id = habit.id))
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(habit_progress.id = habits_progresses.id)
|
||||
AND (habit.user_id = auth.uid())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
264
data/infrastructure/repositories/supabase/seed.sql
Normal file
264
data/infrastructure/repositories/supabase/seed.sql
Normal file
@ -0,0 +1,264 @@
|
||||
-- Users
|
||||
-- User { email: 'test@test.com', password: 'test1234' }
|
||||
INSERT INTO
|
||||
"auth"."users" (
|
||||
"instance_id",
|
||||
"id",
|
||||
"aud",
|
||||
"role",
|
||||
"email",
|
||||
"encrypted_password",
|
||||
"email_confirmed_at",
|
||||
"invited_at",
|
||||
"confirmation_token",
|
||||
"confirmation_sent_at",
|
||||
"recovery_token",
|
||||
"recovery_sent_at",
|
||||
"email_change_token_new",
|
||||
"email_change",
|
||||
"email_change_sent_at",
|
||||
"last_sign_in_at",
|
||||
"raw_app_meta_data",
|
||||
"raw_user_meta_data",
|
||||
"is_super_admin",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"phone",
|
||||
"phone_confirmed_at",
|
||||
"phone_change",
|
||||
"phone_change_token",
|
||||
"phone_change_sent_at",
|
||||
"email_change_token_current",
|
||||
"email_change_confirm_status",
|
||||
"banned_until",
|
||||
"reauthentication_token",
|
||||
"reauthentication_sent_at",
|
||||
"is_sso_user",
|
||||
"deleted_at"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'authenticated',
|
||||
'authenticated',
|
||||
'test@test.com',
|
||||
crypt('test1234', gen_salt('bf')),
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
'',
|
||||
NULL,
|
||||
NULL,
|
||||
'{"provider": "email", "providers": ["email"]}',
|
||||
'{}',
|
||||
NULL,
|
||||
timezone('utc' :: text, NOW()),
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
NULL,
|
||||
'',
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
0,
|
||||
NULL,
|
||||
'',
|
||||
NULL,
|
||||
false,
|
||||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"auth"."identities" (
|
||||
"id",
|
||||
"user_id",
|
||||
"identity_data",
|
||||
"provider",
|
||||
"provider_id",
|
||||
"last_sign_in_at",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'{"sub": "ab054ee9-fbb4-473e-942b-bbf4415f4bef", "email": "test@test.com", "display_name": "Test"}',
|
||||
'email',
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
timezone('utc' :: text, NOW()),
|
||||
timezone('utc' :: text, NOW()),
|
||||
timezone('utc' :: text, NOW())
|
||||
);
|
||||
|
||||
-- Habits
|
||||
INSERT INTO
|
||||
"public"."habits" (
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
color,
|
||||
icon,
|
||||
start_date,
|
||||
end_date,
|
||||
goal_frequency,
|
||||
goal_target,
|
||||
goal_target_unit
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
1,
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'Wake up at 07h00',
|
||||
'#006CFF',
|
||||
'bed',
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'daily',
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"public"."habits" (
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
color,
|
||||
icon,
|
||||
start_date,
|
||||
end_date,
|
||||
goal_frequency,
|
||||
goal_target,
|
||||
goal_target_unit
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
2,
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'Learn English',
|
||||
'#EB4034',
|
||||
'language',
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'daily',
|
||||
30,
|
||||
'minutes'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"public"."habits" (
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
color,
|
||||
icon,
|
||||
start_date,
|
||||
end_date,
|
||||
goal_frequency,
|
||||
goal_target,
|
||||
goal_target_unit
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
3,
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'Walk',
|
||||
'#228B22',
|
||||
'person-walking',
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'daily',
|
||||
5000,
|
||||
'steps'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"public"."habits" (
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
color,
|
||||
icon,
|
||||
start_date,
|
||||
end_date,
|
||||
goal_frequency,
|
||||
goal_target,
|
||||
goal_target_unit
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
4,
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'Clean the house',
|
||||
'#808080',
|
||||
'broom',
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'weekly',
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"public"."habits" (
|
||||
id,
|
||||
user_id,
|
||||
name,
|
||||
color,
|
||||
icon,
|
||||
start_date,
|
||||
end_date,
|
||||
goal_frequency,
|
||||
goal_target,
|
||||
goal_target_unit
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
5,
|
||||
'ab054ee9-fbb4-473e-942b-bbf4415f4bef',
|
||||
'Solve Programming Challenges',
|
||||
'#DE3163',
|
||||
'code',
|
||||
timezone('utc' :: text, NOW()),
|
||||
NULL,
|
||||
'monthly',
|
||||
5,
|
||||
'challenges'
|
||||
);
|
||||
|
||||
-- Habits Progresses
|
||||
INSERT INTO
|
||||
"public"."habits_progresses" (
|
||||
id,
|
||||
habit_id,
|
||||
date,
|
||||
goal_progress
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
1,
|
||||
4,
|
||||
timezone('utc' :: text, NOW()),
|
||||
1
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
"public"."habits_progresses" (
|
||||
id,
|
||||
habit_id,
|
||||
date,
|
||||
goal_progress
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
2,
|
||||
3,
|
||||
timezone('utc' :: text, NOW()),
|
||||
4733
|
||||
);
|
388
data/infrastructure/repositories/supabase/supabase-types.ts
Normal file
388
data/infrastructure/repositories/supabase/supabase-types.ts
Normal file
@ -0,0 +1,388 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export interface Database {
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
operationName?: string
|
||||
query?: string
|
||||
variables?: Json
|
||||
extensions?: Json
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
habits: {
|
||||
Row: {
|
||||
color: string
|
||||
end_date: string | null
|
||||
goal_frequency: Database["public"]["Enums"]["goal_frequency"]
|
||||
goal_target: number | null
|
||||
goal_target_unit: string | null
|
||||
icon: string
|
||||
id: number
|
||||
name: string
|
||||
start_date: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
color: string
|
||||
end_date?: string | null
|
||||
goal_frequency: Database["public"]["Enums"]["goal_frequency"]
|
||||
goal_target?: number | null
|
||||
goal_target_unit?: string | null
|
||||
icon: string
|
||||
id?: number
|
||||
name: string
|
||||
start_date?: string
|
||||
user_id?: string
|
||||
}
|
||||
Update: {
|
||||
color?: string
|
||||
end_date?: string | null
|
||||
goal_frequency?: Database["public"]["Enums"]["goal_frequency"]
|
||||
goal_target?: number | null
|
||||
goal_target_unit?: string | null
|
||||
icon?: string
|
||||
id?: number
|
||||
name?: string
|
||||
start_date?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "public_habits_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
habits_progresses: {
|
||||
Row: {
|
||||
date: string
|
||||
goal_progress: number
|
||||
habit_id: number
|
||||
id: number
|
||||
}
|
||||
Insert: {
|
||||
date?: string
|
||||
goal_progress: number
|
||||
habit_id: number
|
||||
id?: number
|
||||
}
|
||||
Update: {
|
||||
date?: string
|
||||
goal_progress?: number
|
||||
habit_id?: number
|
||||
id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "public_habits_progresses_habit_id_fkey"
|
||||
columns: ["habit_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "habits"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
goal_frequency: "daily" | "weekly" | "monthly"
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
storage: {
|
||||
Tables: {
|
||||
buckets: {
|
||||
Row: {
|
||||
allowed_mime_types: string[] | null
|
||||
avif_autodetection: boolean | null
|
||||
created_at: string | null
|
||||
file_size_limit: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner: string | null
|
||||
owner_id: string | null
|
||||
public: boolean | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id?: string
|
||||
name?: string
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
migrations: {
|
||||
Row: {
|
||||
executed_at: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
executed_at?: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
executed_at?: string | null
|
||||
hash?: string
|
||||
id?: number
|
||||
name?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
objects: {
|
||||
Row: {
|
||||
bucket_id: string | null
|
||||
created_at: string | null
|
||||
id: string
|
||||
last_accessed_at: string | null
|
||||
metadata: Json | null
|
||||
name: string | null
|
||||
owner: string | null
|
||||
owner_id: string | null
|
||||
path_tokens: string[] | null
|
||||
updated_at: string | null
|
||||
version: string | null
|
||||
}
|
||||
Insert: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Update: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
owner_id?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "objects_bucketId_fkey"
|
||||
columns: ["bucket_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "buckets"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
can_insert_object: {
|
||||
Args: {
|
||||
bucketid: string
|
||||
name: string
|
||||
owner: string
|
||||
metadata: Json
|
||||
}
|
||||
Returns: undefined
|
||||
}
|
||||
extension: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
filename: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
foldername: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: unknown
|
||||
}
|
||||
get_size_by_bucket: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: Array<{
|
||||
size: number
|
||||
bucket_id: string
|
||||
}>
|
||||
}
|
||||
search: {
|
||||
Args: {
|
||||
prefix: string
|
||||
bucketname: string
|
||||
limits?: number
|
||||
levels?: number
|
||||
offsets?: number
|
||||
search?: string
|
||||
sortcolumn?: string
|
||||
sortorder?: string
|
||||
}
|
||||
Returns: Array<{
|
||||
name: string
|
||||
id: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
last_accessed_at: string
|
||||
metadata: Json
|
||||
}>
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PublicSchema = Database[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])
|
||||
? (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
PublicEnumNameOrOptions extends
|
||||
| keyof PublicSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
||||
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
||||
: never
|
11
data/infrastructure/repositories/supabase/supabase.ts
Normal file
11
data/infrastructure/repositories/supabase/supabase.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
|
||||
import type { Database } from "./supabase-types"
|
||||
|
||||
const SUPABASE_URL = process.env["EXPO_PUBLIC_SUPABASE_URL"] ?? ""
|
||||
const SUPABASE_ANON_KEY = process.env["EXPO_PUBLIC_SUPABASE_ANON_KEY"] ?? ""
|
||||
|
||||
export const supabaseClient = createClient<Database>(
|
||||
SUPABASE_URL,
|
||||
SUPABASE_ANON_KEY,
|
||||
)
|
1
docs/ARCHITECTURE.md
Normal file
1
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1 @@
|
||||
# Clean Architecture
|
36
docs/CONVENTIONS.md
Normal file
36
docs/CONVENTIONS.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Conventions développement informatique
|
||||
|
||||
## Linting/Formatting/Tests
|
||||
|
||||
Le code est formaté grâce à [Prettier](https://prettier.io/) et vérifié grâce à [ESLint](https://eslint.org/) et à [TypeScript](https://www.typescriptlang.org/) pour s'assurer que le code respecte les bonnes pratiques de développement, et détecter en amont les possibles erreurs.
|
||||
|
||||
Nous utilisons également [Jest](https://jestjs.io/) pour les tests automatisés.
|
||||
|
||||
```sh
|
||||
# Lint
|
||||
npm run lint:prettier
|
||||
npm run lint:eslint
|
||||
npm run lint:typescript
|
||||
|
||||
# Test
|
||||
npm run test
|
||||
```
|
||||
|
||||
Une pipeline CI ([`.gitlab-ci.yml`](.gitlab-ci.yml)) est en place pour vérifier que le code respecte ces bonnes pratiques et que les tests passent.
|
||||
|
||||
## GitFlow
|
||||
|
||||
Le projet suit la convention [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) reposant sur 2 branches principales:
|
||||
|
||||
- `main` (ou `master`): Contient le code de la dernière version stable et déployé en production.
|
||||
- `develop`: Contient le code en cours de développement. Les nouvelles fonctionnalités et les correctifs de bugs sont fusionnés ici.
|
||||
|
||||
## Convention des commits
|
||||
|
||||
Les commits respectent la convention [Conventional Commits](https://www.conventionalcommits.org/) et [Semantic Versioning](https://semver.org/) pour la gestion des versions et des releases en fonction des commits.
|
||||
|
||||
Les commits doivent être **atomiques** c'est à dire qu'il respecte 3 règles:
|
||||
|
||||
- Ne concerne qu'un seul sujet (une fonctionnalité, une correction de bug, etc.)
|
||||
- Doit avoir un message clair et concis
|
||||
- Ne doit pas rendre de dépôt "incohérent" (bloque la CI du projet)
|
42
docs/MLD.md
Normal file
42
docs/MLD.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Modèle Logique des Données (MLD)
|
||||
|
||||
## Introduction
|
||||
|
||||
Le **Modèle Logique des Données (MLD)** est une représentation de la structure de la base de données de l'application.
|
||||
|
||||
On représente ainsi les données sous la forme suivante:
|
||||
|
||||
- Chaque table est représentée par un bloc.
|
||||
- Le nom de la table est écrit en **gras**.
|
||||
- Les champs sont listés en dessous du nom de la table.
|
||||
- Les clés primaires sont <u>soulignées</u> et placées au début de la liste des champs.
|
||||
- Les clés étrangères sont préfixées par un dièse (#), et placées après les clés primaires. Les clés étrangères sont
|
||||
suivies entre parenthèses du nom de la table suivi d'une flèche (->) et du nom du champ de la table référencée.
|
||||
|
||||
## Modèle
|
||||
|
||||
- **users**
|
||||
- <u>id</u>
|
||||
- email (unique)
|
||||
- display_name
|
||||
- encrypted_password
|
||||
- role
|
||||
- email_confirmed_at (nullable)
|
||||
- created_at
|
||||
- updated_at
|
||||
- **habits**
|
||||
- <u>id</u>
|
||||
- #user_id (Users->id)
|
||||
- name
|
||||
- color
|
||||
- icon
|
||||
- start_date
|
||||
- end_date (nullable)
|
||||
- goal_frequency (enum: `daily`, `weekly`, `monthly`)
|
||||
- goal_target (nullable)
|
||||
- goal_target_unit (nullable)
|
||||
- **habits_progresses**
|
||||
- <u>id</u>
|
||||
- #habit_id (Habits->id)
|
||||
- date
|
||||
- goal_progress
|
21
hooks/usePresenterState.ts
Normal file
21
hooks/usePresenterState.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import type { Presenter } from "@/data/infrastructure/presenters/_Presenter"
|
||||
|
||||
export const usePresenterState = <S>(presenter: Presenter<S>): S => {
|
||||
const [state, setState] = useState<S>(presenter.initialState)
|
||||
|
||||
useEffect(() => {
|
||||
const presenterSubscription = (state: S): void => {
|
||||
setState(state)
|
||||
}
|
||||
|
||||
presenter.subscribe(presenterSubscription)
|
||||
|
||||
return () => {
|
||||
return presenter.unsubscribe(presenterSubscription)
|
||||
}
|
||||
}, [presenter])
|
||||
|
||||
return state
|
||||
}
|
4339
package-lock.json
generated
4339
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -14,23 +14,26 @@
|
||||
"lint:typescript": "tsc --noEmit",
|
||||
"lint:staged": "lint-staged",
|
||||
"test": "jest --coverage --reporters=default --reporters=jest-junit",
|
||||
"supabase": "supabase --workdir \"./data/infrastructure/repositories\"",
|
||||
"postinstall": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "14.0.0",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-navigation/native": "6.1.10",
|
||||
"expo": "50.0.7",
|
||||
"@react-navigation/native": "6.1.16",
|
||||
"@supabase/supabase-js": "2.39.8",
|
||||
"expo": "50.0.13",
|
||||
"expo-font": "11.10.3",
|
||||
"expo-linking": "6.2.2",
|
||||
"expo-router": "3.4.7",
|
||||
"expo-router": "3.4.8",
|
||||
"expo-splash-screen": "0.26.4",
|
||||
"expo-status-bar": "1.11.1",
|
||||
"expo-system-ui": "2.9.3",
|
||||
"expo-web-browser": "12.8.2",
|
||||
"immer": "10.0.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.73.4",
|
||||
"react-native": "0.73.5",
|
||||
"react-native-paper": "5.12.3",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "3.29.0",
|
||||
@ -38,33 +41,35 @@
|
||||
"react-native-web": "0.19.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.9",
|
||||
"@commitlint/cli": "18.6.1",
|
||||
"@commitlint/config-conventional": "18.6.2",
|
||||
"@babel/core": "7.24.0",
|
||||
"@commitlint/cli": "19.1.0",
|
||||
"@commitlint/config-conventional": "19.1.0",
|
||||
"@testing-library/react-native": "12.4.3",
|
||||
"@total-typescript/ts-reset": "0.5.1",
|
||||
"@tsconfig/strictest": "2.0.3",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/react": "18.2.55",
|
||||
"@types/node": "20.11.28",
|
||||
"@types/react": "18.2.66",
|
||||
"@types/react-test-renderer": "18.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "7.0.1",
|
||||
"@typescript-eslint/parser": "7.0.1",
|
||||
"eslint": "8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-conventions": "14.1.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-react": "7.34.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-native": "4.1.0",
|
||||
"eslint-plugin-unicorn": "51.0.1",
|
||||
"husky": "9.0.11",
|
||||
"jest": "29.7.0",
|
||||
"jest-expo": "50.0.2",
|
||||
"jest-expo": "50.0.4",
|
||||
"jest-junit": "16.0.0",
|
||||
"lint-staged": "15.2.2",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"typescript": "5.3.3"
|
||||
"supabase": "1.148.6",
|
||||
"typescript": "5.4.2"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user