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
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.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.
|
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
|
### Membres du Groupe 7
|
||||||
|
|
||||||
- [Théo LUDWIG](https://git.unistra.fr/t.ludwig)
|
- [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 RUMPLER](https://git.unistra.fr/m.rumpler)
|
||||||
- [Maxime RICHARD](https://git.unistra.fr/maximerichard)
|
- [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
|
## Développement du projet en local
|
||||||
|
|
||||||
### Prérequis
|
### 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
|
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||||
- [npm](https://www.npmjs.com/) >= 10.0.0
|
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||||
- [Expo Go](https://expo.io/client)
|
- [Expo Go](https://expo.io/client)
|
||||||
|
- [Docker](https://www.docker.com/) (facultatif, utilisé pour lancer [Supabase](https://supabase.io/) en local)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@ -30,6 +44,12 @@ Un tracker d'habitudes pour performer au boulot et dans la vie de tous les jours
|
|||||||
# Cloner le projet
|
# Cloner le projet
|
||||||
git clone git@git.unistra.fr:rrll/p61-project.git
|
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
|
# Installer les dépendances
|
||||||
npm clean-install
|
npm clean-install
|
||||||
```
|
```
|
||||||
@ -40,37 +60,10 @@ npm clean-install
|
|||||||
npm run start
|
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.
|
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.
|
||||||
|
|
||||||
Nous utilisons également [Jest](https://jestjs.io/) pour les tests automatisés.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Lint
|
npm run supabase
|
||||||
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)
|
|
||||||
|
2
app.json
2
app.json
@ -5,7 +5,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "myapp",
|
"scheme": "p61-project",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/images/splashscreen.jpg",
|
"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 { StyleSheet, Text, View } from "react-native"
|
||||||
import { Button } from "react-native-paper"
|
|
||||||
import { SafeAreaView } from "react-native-safe-area-context"
|
import { SafeAreaView } from "react-native-safe-area-context"
|
||||||
|
|
||||||
|
import { useHabitsTracker } from "@/contexts/HabitsTracker"
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
|
const { habitsTrackerPresenterState } = useHabitsTracker()
|
||||||
|
const { habitsTracker } = habitsTrackerPresenterState
|
||||||
|
const { habitProgressHistories } = habitsTracker
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<Button
|
{habitProgressHistories.map((progressHistory) => {
|
||||||
mode="contained"
|
const { habit } = progressHistory
|
||||||
onPress={() => {
|
|
||||||
return console.log("Pressed")
|
return (
|
||||||
}}
|
<View key={habit.id}>
|
||||||
>
|
<Text>{habit.name}</Text>
|
||||||
Press me
|
</View>
|
||||||
</Button>
|
)
|
||||||
|
})}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { StatusBar } from "expo-status-bar"
|
|||||||
import CanterburyFont from "../assets/fonts/Canterbury.ttf"
|
import CanterburyFont from "../assets/fonts/Canterbury.ttf"
|
||||||
import GeoramFont from "../assets/fonts/Georama-Black.ttf"
|
import GeoramFont from "../assets/fonts/Georama-Black.ttf"
|
||||||
import SpaceMonoFont from "../assets/fonts/SpaceMono-Regular.ttf"
|
import SpaceMonoFont from "../assets/fonts/SpaceMono-Regular.ttf"
|
||||||
|
import { HabitsTrackerProvider } from "@/contexts/HabitsTracker"
|
||||||
|
|
||||||
export { ErrorBoundary } from "expo-router"
|
export { ErrorBoundary } from "expo-router"
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ const RootLayout: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<HabitsTrackerProvider>
|
||||||
<PaperProvider
|
<PaperProvider
|
||||||
theme={{
|
theme={{
|
||||||
...DefaultTheme,
|
...DefaultTheme,
|
||||||
@ -68,6 +70,7 @@ const RootLayout: React.FC = () => {
|
|||||||
|
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
</PaperProvider>
|
</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:typescript": "tsc --noEmit",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test": "jest --coverage --reporters=default --reporters=jest-junit",
|
"test": "jest --coverage --reporters=default --reporters=jest-junit",
|
||||||
|
"supabase": "supabase --workdir \"./data/infrastructure/repositories\"",
|
||||||
"postinstall": "husky"
|
"postinstall": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "14.0.0",
|
"@expo/vector-icons": "14.0.0",
|
||||||
"@react-native-async-storage/async-storage": "1.21.0",
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
"@react-navigation/native": "6.1.10",
|
"@react-navigation/native": "6.1.16",
|
||||||
"expo": "50.0.7",
|
"@supabase/supabase-js": "2.39.8",
|
||||||
|
"expo": "50.0.13",
|
||||||
"expo-font": "11.10.3",
|
"expo-font": "11.10.3",
|
||||||
"expo-linking": "6.2.2",
|
"expo-linking": "6.2.2",
|
||||||
"expo-router": "3.4.7",
|
"expo-router": "3.4.8",
|
||||||
"expo-splash-screen": "0.26.4",
|
"expo-splash-screen": "0.26.4",
|
||||||
"expo-status-bar": "1.11.1",
|
"expo-status-bar": "1.11.1",
|
||||||
"expo-system-ui": "2.9.3",
|
"expo-system-ui": "2.9.3",
|
||||||
"expo-web-browser": "12.8.2",
|
"expo-web-browser": "12.8.2",
|
||||||
|
"immer": "10.0.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "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-paper": "5.12.3",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "3.29.0",
|
"react-native-screens": "3.29.0",
|
||||||
@ -38,33 +41,35 @@
|
|||||||
"react-native-web": "0.19.10"
|
"react-native-web": "0.19.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.23.9",
|
"@babel/core": "7.24.0",
|
||||||
"@commitlint/cli": "18.6.1",
|
"@commitlint/cli": "19.1.0",
|
||||||
"@commitlint/config-conventional": "18.6.2",
|
"@commitlint/config-conventional": "19.1.0",
|
||||||
"@testing-library/react-native": "12.4.3",
|
"@testing-library/react-native": "12.4.3",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
"@tsconfig/strictest": "2.0.3",
|
"@tsconfig/strictest": "2.0.3",
|
||||||
"@types/jest": "29.5.12",
|
"@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",
|
"@types/react-test-renderer": "18.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "7.0.1",
|
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||||
"@typescript-eslint/parser": "7.0.1",
|
"@typescript-eslint/parser": "7.2.0",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-conventions": "14.1.0",
|
"eslint-config-conventions": "14.1.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-prettier": "5.1.3",
|
"eslint-plugin-prettier": "5.1.3",
|
||||||
"eslint-plugin-promise": "6.1.1",
|
"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-hooks": "4.6.0",
|
||||||
"eslint-plugin-react-native": "4.1.0",
|
"eslint-plugin-react-native": "4.1.0",
|
||||||
"eslint-plugin-unicorn": "51.0.1",
|
"eslint-plugin-unicorn": "51.0.1",
|
||||||
"husky": "9.0.11",
|
"husky": "9.0.11",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-expo": "50.0.2",
|
"jest-expo": "50.0.4",
|
||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"lint-staged": "15.2.2",
|
"lint-staged": "15.2.2",
|
||||||
"react-test-renderer": "18.2.0",
|
"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