chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

6
packages/modules/api-key/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,23 @@
# @medusajs/api-key
## 0.1.2
### Patch Changes
- [#6851](https://github.com/medusajs/medusa/pull/6851) [`ea8d9d4d42`](https://github.com/medusajs/medusa/commit/ea8d9d4d42210a5598b308656922c0e93c90b7c8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - feat: API key sales channel link
- Updated dependencies [[`0c0b425de7`](https://github.com/medusajs/medusa/commit/0c0b425de7b154b80b712ab17b16215cf62d1e83), [`8d356217bd`](https://github.com/medusajs/medusa/commit/8d356217bd31c97a196e861ee243822a4d924df7), [`1eeb1e9de3`](https://github.com/medusajs/medusa/commit/1eeb1e9de3e0b571735437b00968ee96e4aabad5), [`20e8df914e`](https://github.com/medusajs/medusa/commit/20e8df914ec5fdf8d562d4fa84f72c58c7056195), [`27f4f0d724`](https://github.com/medusajs/medusa/commit/27f4f0d7243367c2dfc6012bf1f6b7400a77ec7b), [`e0b02a1012`](https://github.com/medusajs/medusa/commit/e0b02a1012981c29830d7779f59ebe805bbfd137), [`e944a627f0`](https://github.com/medusajs/medusa/commit/e944a627f074fb39a56f4bc7b3d6d315736ebf7c), [`1a48fe0282`](https://github.com/medusajs/medusa/commit/1a48fe0282a8bc1f8548a4736255e457d173da09), [`86f499de2f`](https://github.com/medusajs/medusa/commit/86f499de2f31356ab36ad5e93f27345443b3e5f6), [`09a2220569`](https://github.com/medusajs/medusa/commit/09a22205693da62fbf8fd450535d5024cb9c01d1), [`78f603e4f1`](https://github.com/medusajs/medusa/commit/78f603e4f18c9d16f4b58a2189c959026453d8b2), [`cc557c8752`](https://github.com/medusajs/medusa/commit/cc557c8752fd0554f5a1b58522d9a88dc43a8509), [`dd35a4dbff`](https://github.com/medusajs/medusa/commit/dd35a4dbff10c86ea3c5f7f817c18b6e60d599e3), [`58c68f6715`](https://github.com/medusajs/medusa/commit/58c68f67156e993255fbc25d91db15ae23bc95c0), [`1bcb13f892`](https://github.com/medusajs/medusa/commit/1bcb13f892bc61db21b3fc6bdbce85f747aeec4c), [`82a176e30e`](https://github.com/medusajs/medusa/commit/82a176e30e47a7d11caaf31c3023bd8db588b465), [`11517f0faf`](https://github.com/medusajs/medusa/commit/11517f0fafdf00af256240448b58d149d8b6f600), [`62b9dcc6c1`](https://github.com/medusajs/medusa/commit/62b9dcc6c1ce46aadb7944215006c12da3c9f619), [`5d9aea053c`](https://github.com/medusajs/medusa/commit/5d9aea053ce6e04f242f86fb9053c13dec515d5b), [`e26cda4b6a`](https://github.com/medusajs/medusa/commit/e26cda4b6afb7fb25f0b0a7a7ce20b7f914d35db), [`bc06ad2db4`](https://github.com/medusajs/medusa/commit/bc06ad2db48c999023ab823fefc1375196976e9b), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`232322d035`](https://github.com/medusajs/medusa/commit/232322d03515f81e56867ff8c765b8409399ee68), [`38c971f111`](https://github.com/medusajs/medusa/commit/38c971f111af69f176e7e9892eb59f5bae831fa7), [`45c49e89f2`](https://github.com/medusajs/medusa/commit/45c49e89f28123ef622fc1c07253bae94fd74875), [`528ef4ca90`](https://github.com/medusajs/medusa/commit/528ef4ca90bb2cf6173dccc9fd6a9f9932ff9b76), [`65794f4bb5`](https://github.com/medusajs/medusa/commit/65794f4bb56e4fd3f0ccb7656a948f856f05324e), [`93ef94cad3`](https://github.com/medusajs/medusa/commit/93ef94cad3ddc5b6973b4e48e422b0aa0e6ddbbe), [`4cf71af07d`](https://github.com/medusajs/medusa/commit/4cf71af07d1807c83df3889c1774f82cbd1b9a6f), [`4b57c5d286`](https://github.com/medusajs/medusa/commit/4b57c5d286f9dc6e2098c67e9fecb0d93175b5a1), [`c78915c7c5`](https://github.com/medusajs/medusa/commit/c78915c7c5e91a99c1b1bae932656c8d86b17daf), [`18f3aacee6`](https://github.com/medusajs/medusa/commit/18f3aacee6752854d377faa806f4cc67bc71456b), [`667c8609cc`](https://github.com/medusajs/medusa/commit/667c8609ccf3850f5df8cf784723a95bd0d6d2a6), [`f175cac4af`](https://github.com/medusajs/medusa/commit/f175cac4af63b71066a8398ecf9beaa6f28b20cc), [`0a9b9b073d`](https://github.com/medusajs/medusa/commit/0a9b9b073dd2d3f4aa5e5cb1c16e2221a7200e0d), [`a6562d2a41`](https://github.com/medusajs/medusa/commit/a6562d2a41453cbe7aa43be352c4924e3e4c79d5), [`00e6b21bb5`](https://github.com/medusajs/medusa/commit/00e6b21bb50dbc886bc37ad052a1c40ce865294e), [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9), [`1c6ba4468e`](https://github.com/medusajs/medusa/commit/1c6ba4468eab1440931c88929affd5b4c593f377)]:
- @medusajs/types@1.11.16
- @medusajs/modules-sdk@1.12.11
- @medusajs/utils@1.11.9
## 0.1.1
### Patch Changes
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/types@1.11.14
- @medusajs/utils@1.11.7
- @medusajs/modules-sdk@1.12.9

View File

@@ -0,0 +1 @@
# API Key Module

View File

@@ -0,0 +1,14 @@
import { CreateApiKeyDTO } from "@types"
import { ApiKeyType } from "@medusajs/utils"
export const createSecretKeyFixture: CreateApiKeyDTO = {
title: "Secret key",
type: ApiKeyType.SECRET,
created_by: "test",
}
export const createPublishableKeyFixture: CreateApiKeyDTO = {
title: "Test API Key",
type: ApiKeyType.PUBLISHABLE,
created_by: "test",
}

View File

@@ -0,0 +1,356 @@
import crypto from "crypto"
import { Modules } from "@medusajs/modules-sdk"
import { IApiKeyModuleService } from "@medusajs/types"
import { ApiKeyType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import {
createSecretKeyFixture,
createPublishableKeyFixture,
} from "../__fixtures__"
jest.setTimeout(100000)
const mockPublishableKeyBytes = () => {
jest.spyOn(crypto, "randomBytes").mockImplementationOnce(() => {
return Buffer.from(
"44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
"hex"
)
})
}
const mockSecretKeyBytes = () => {
jest
.spyOn(crypto, "randomBytes")
.mockImplementationOnce(() => {
return Buffer.from(
"44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
"hex"
)
})
.mockImplementationOnce(() => {
return Buffer.from("44de31ebcf085fa423fc584aa8540670", "hex")
})
}
moduleIntegrationTestRunner({
moduleName: Modules.API_KEY,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IApiKeyModuleService>) => {
afterEach(() => {
jest.restoreAllMocks()
})
describe("API Key Module Service", () => {
describe("creating a publishable API key", () => {
it("should create it successfully", async function () {
mockPublishableKeyBytes()
const apiKey = await service.create(createPublishableKeyFixture)
expect(apiKey).toEqual(
expect.objectContaining({
title: "Test API Key",
type: ApiKeyType.PUBLISHABLE,
salt: undefined,
created_by: "test",
last_used_at: null,
revoked_by: null,
revoked_at: null,
redacted: "pk_44d***3be",
token:
"pk_44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
})
)
})
})
describe("creating a secret API key", () => {
it("should get created successfully", async function () {
mockSecretKeyBytes()
const apiKey = await service.create(createSecretKeyFixture)
expect(apiKey).toEqual(
expect.objectContaining({
title: "Secret key",
type: ApiKeyType.SECRET,
salt: undefined,
created_by: "test",
last_used_at: null,
revoked_by: null,
revoked_at: null,
redacted: "sk_44d***3be",
token:
"sk_44de31ebcf085fa423fc584aa854067025e937a79edb565f472404345f0f23be",
})
)
})
it("should only allow creating one active token", async function () {
expect(
service.create([createSecretKeyFixture, createSecretKeyFixture])
).rejects.toThrow(
"You can only create one secret key at a time. You tried to create 2 secret keys."
)
await service.create(createSecretKeyFixture)
const err = await service
.create(createSecretKeyFixture)
.catch((e) => e)
expect(err.message).toEqual(
"You can only have one active secret key a time. Revoke or delete your existing key before creating a new one."
)
})
it("should allow for at most two tokens, where one is revoked", async function () {
const firstApiKey = await service.create(createSecretKeyFixture)
await service.revoke(
{ id: firstApiKey.id },
{
revoked_by: "test",
}
)
await service.create(createSecretKeyFixture)
const err = await service
.create(createSecretKeyFixture)
.catch((e) => e)
expect(err.message).toEqual(
"You can only have one active secret key a time. Revoke or delete your existing key before creating a new one."
)
})
})
describe("revoking API keys", () => {
it("should have the revoked at and revoked by set when a key is revoked", async function () {
const firstApiKey = await service.create(createSecretKeyFixture)
const revokedKey = await service.revoke(firstApiKey.id, {
revoked_by: "test",
})
expect(revokedKey).toEqual(
expect.objectContaining({
revoked_by: "test",
revoked_at: expect.any(Date),
})
)
})
it("should be able to revoke a key in the future", async function () {
const now = Date.parse("2021-01-01T00:00:00Z")
const hourInSec = 3600
jest.useFakeTimers().setSystemTime(now)
const createdKey = await service.create(createSecretKeyFixture)
const revokedKey = await service.revoke(createdKey.id, {
revoked_by: "test",
revoke_in: hourInSec,
})
expect(revokedKey).toEqual(
expect.objectContaining({
revoked_by: "test",
revoked_at: new Date(now + hourInSec * 1000),
})
)
jest.useRealTimers()
})
it("should do nothing if the revokal list is empty", async function () {
const firstApiKey = await service.create(createSecretKeyFixture)
let revokedKeys = await service.revoke([])
expect(revokedKeys).toHaveLength(0)
const apiKey = await service.retrieve(firstApiKey.id)
expect(apiKey.revoked_at).toBeFalsy()
expect(apiKey.revoked_by).toBeFalsy()
})
it("should not allow revoking an already revoked API key", async function () {
const firstApiKey = await service.create(createSecretKeyFixture)
await service.revoke(firstApiKey.id, {
revoked_by: "test",
})
const err = await service
.revoke(firstApiKey.id, {
revoked_by: "test2",
})
.catch((e) => e)
expect(err.message).toEqual(
`There are 1 secret keys that are already revoked.`
)
})
})
describe("updating an API key", () => {
it("should update the name successfully", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const updatedApiKey = await service.update(createdApiKey.id, {
title: "New Name",
})
expect(updatedApiKey.title).toEqual("New Name")
})
it("should not reflect any updates on other fields", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const updatedApiKey = await service.update(createdApiKey.id, {
title: createdApiKey.title,
revoked_by: "test",
revoked_at: new Date(),
last_used_at: new Date(),
})
// These should not be returned on an update
createdApiKey.token = ""
expect(createdApiKey).toEqual(updatedApiKey)
})
})
describe("deleting API keys", () => {
it("should successfully delete existing api keys", async function () {
const createdApiKeys = await service.create([
createPublishableKeyFixture,
createSecretKeyFixture,
])
await service.delete([createdApiKeys[0].id, createdApiKeys[1].id])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(0)
})
})
describe("authenticating with API keys", () => {
it("should authenticate a secret key successfully", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeTruthy()
expect(authenticated.title).toEqual(createSecretKeyFixture.title)
})
it("should authenticate with a token to be revoked in the future", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
// We simulate setting the revoked_at in the future here
jest.useFakeTimers().setSystemTime(new Date().setFullYear(3000))
await service.revoke(createdApiKey.id, {
revoked_by: "test",
})
jest.useRealTimers()
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeTruthy()
expect(authenticated.title).toEqual(createdApiKey.title)
})
it("should not authenticate a publishable key", async function () {
const createdApiKey = await service.create(
createPublishableKeyFixture
)
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeFalsy()
})
it("should not authenticate with a non-existent token", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
const authenticated = await service.authenticate("some-token")
expect(authenticated).toBeFalsy()
})
it("should not authenticate with a revoked token", async function () {
const createdApiKey = await service.create(createSecretKeyFixture)
await service.revoke(createdApiKey.id, {
revoked_by: "test",
})
const authenticated = await service.authenticate(createdApiKey.token)
expect(authenticated).toBeFalsy()
})
})
describe("retrieving API keys", () => {
it("should successfully return all existing api keys", async function () {
await service.create([
createPublishableKeyFixture,
createSecretKeyFixture,
])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(2)
})
it("should only return keys with matching token", async function () {
const created = await service.create([
createPublishableKeyFixture,
createPublishableKeyFixture,
])
const apiKeysInDatabase = await service.list({
token: created[0].token,
})
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toEqual(created[0].token)
})
it("should not return the token and salt for secret keys when listing", async function () {
await service.create([createSecretKeyFixture])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeFalsy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should return the token for publishable keys when listing", async function () {
await service.create([createPublishableKeyFixture])
const apiKeysInDatabase = await service.list()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeTruthy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should not return the token and salt for secret keys when listing and counting", async function () {
await service.create([createSecretKeyFixture])
const [apiKeysInDatabase] = await service.listAndCount()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeFalsy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should return the token for publishable keys when listing and counting", async function () {
await service.create([createPublishableKeyFixture])
const [apiKeysInDatabase] = await service.listAndCount()
expect(apiKeysInDatabase).toHaveLength(1)
expect(apiKeysInDatabase[0].token).toBeTruthy()
expect(apiKeysInDatabase[0].salt).toBeFalsy()
})
it("should not return the token and salt for secret keys when retrieving", async function () {
const [createdApiKey] = await service.create([createSecretKeyFixture])
const apiKeyInDatabase = await service.retrieve(createdApiKey.id)
expect(apiKeyInDatabase.token).toBeFalsy()
expect(apiKeyInDatabase.salt).toBeFalsy()
})
it("should return the token for publishable keys when retrieving", async function () {
const [createdApiKey] = await service.create([
createPublishableKeyFixture,
])
const apiKeyInDatabase = await service.retrieve(createdApiKey.id)
expect(apiKeyInDatabase.token).toBeTruthy()
expect(apiKeyInDatabase.salt).toBeFalsy()
})
})
})
},
})

View File

@@ -0,0 +1,20 @@
module.exports = {
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
},
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.spec.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
}

View File

@@ -0,0 +1,12 @@
import * as entities from "./src/models"
import { TSMigrationGenerator } from "@medusajs/utils"
module.exports = {
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-api-key",
type: "postgresql",
migrations: {
generator: TSMigrationGenerator,
},
}

View File

@@ -0,0 +1,61 @@
{
"name": "@medusajs/api-key",
"version": "0.1.2",
"description": "Medusa API Key module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"bin": {
"medusa-api-key-seed": "dist/scripts/bin/run-seed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/api-key"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
},
"devDependencies": {
"@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"medusa-test-utils": "^1.1.44",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.11",
"@medusajs/types": "^1.11.16",
"@medusajs/utils": "^1.11.9",
"@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"dotenv": "^16.4.5",
"knex": "2.4.2"
}
}

View File

@@ -0,0 +1,14 @@
import { moduleDefinition } from "./module-definition"
import { initializeFactory, Modules } from "@medusajs/modules-sdk"
export * from "./types"
export * from "./models"
export * from "./services"
export const initialize = initializeFactory({
moduleName: Modules.API_KEY,
moduleDefinition,
})
export const runMigrations = moduleDefinition.runMigrations
export const revertMigration = moduleDefinition.revertMigration
export default moduleDefinition

View File

@@ -0,0 +1,31 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import ApiKey from "./models/api-key"
export const LinkableKeys: Record<string, string> = {
api_key_id: ApiKey.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.API_KEY,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [
{
name: ["api_key", "api_keys"],
args: { entity: ApiKey.name },
},
],
} as ModuleJoinerConfig

View File

@@ -0,0 +1,150 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"token": {
"name": "token",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"salt": {
"name": "salt",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"redacted": {
"name": "redacted",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"title": {
"name": "title",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"type": {
"name": "type",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"last_used_at": {
"name": "last_used_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"created_by": {
"name": "created_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"revoked_by": {
"name": "revoked_by",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"revoked_at": {
"name": "revoked_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "api_key",
"schema": "public",
"indexes": [
{
"keyName": "IDX_api_key_token_unique",
"columnNames": [
"token"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token)"
},
{
"keyName": "IDX_api_key_type",
"columnNames": [
"type"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type)"
},
{
"keyName": "api_key_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
}
]
}

View File

@@ -0,0 +1,15 @@
import { Migration } from "@mikro-orm/migrations"
export class InitialSetup20240221144943 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table if not exists "api_key" ("id" text not null, "token" text not null, "salt" text not null, "redacted" text not null, "title" text not null, "type" text not null, "last_used_at" timestamptz null, "created_by" text not null, "created_at" timestamptz not null default now(), "revoked_by" text null, "revoked_at" timestamptz null, constraint "api_key_pkey" primary key ("id"));'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_api_key_token_unique" ON "api_key" (token);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_api_key_type" ON "api_key" (type);'
)
}
}

View File

@@ -0,0 +1,86 @@
import {
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
BeforeCreate,
Entity,
Enum,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
const TypeIndex = createPsqlIndexStatementHelper({
tableName: "api_key",
columns: "type",
})
const TokenIndex = createPsqlIndexStatementHelper({
tableName: "api_key",
columns: "token",
unique: true,
})
@Entity()
export default class ApiKey {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
@TokenIndex.MikroORMIndex()
token: string
@Property({ columnType: "text" })
salt: string
@Searchable()
@Property({ columnType: "text" })
redacted: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
@Enum({ items: ["publishable", "secret"] })
@TypeIndex.MikroORMIndex()
type: "publishable" | "secret"
@Property({
columnType: "timestamptz",
nullable: true,
})
last_used_at: Date | null = null
@Property({ columnType: "text" })
created_by: string
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({ columnType: "text", nullable: true })
revoked_by: string | null = null
@Property({
columnType: "timestamptz",
nullable: true,
})
revoked_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "apk")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "apk")
}
}

View File

@@ -0,0 +1 @@
export { default as ApiKey } from "./api-key"

View File

@@ -0,0 +1,44 @@
import { ModuleExports } from "@medusajs/types"
import * as ModuleServices from "@services"
import { ApiKeyModuleService } from "@services"
import { Modules } from "@medusajs/modules-sdk"
import * as Models from "@models"
import * as ModuleModels from "@models"
import { ModulesSdkUtils } from "@medusajs/utils"
import * as ModuleRepositories from "@repositories"
const migrationScriptOptions = {
moduleName: Modules.API_KEY,
models: Models,
pathToMigrations: __dirname + "/migrations",
}
const runMigrations = ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: ModuleModels,
moduleRepositories: ModuleRepositories,
moduleServices: ModuleServices,
})
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleName: Modules.API_KEY,
moduleModels: Object.values(Models),
migrationsPath: __dirname + "/migrations",
})
const service = ApiKeyModuleService
const loaders = [containerLoader, connectionLoader] as any
export const moduleDefinition: ModuleExports = {
service,
loaders,
revertMigration,
runMigrations,
}

View File

@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env node
import { ModulesSdkUtils } from "@medusajs/utils"
import { Modules } from "@medusajs/modules-sdk"
import * as Models from "@models"
import { EOL } from "os"
const args = process.argv
const path = args.pop() as string
export default (async () => {
const { config } = await import("dotenv")
config()
if (!path) {
throw new Error(
`filePath is required.${EOL}Example: medusa-api-key-seed <filePath>`
)
}
const run = ModulesSdkUtils.buildSeedScript({
moduleName: Modules.API_KEY,
models: Models,
pathToMigrations: __dirname + "/../../migrations",
seedHandler: async ({ manager, data }) => {
// TODO: Add seed logic
},
})
await run({ path })
})()

View File

@@ -0,0 +1,5 @@
describe("noop", function () {
it("should run", function () {
expect(true).toBe(true)
})
})

View File

@@ -0,0 +1,592 @@
import crypto from "crypto"
import util from "util"
import {
Context,
DAL,
ApiKeyTypes,
IApiKeyModuleService,
ModulesSdkTypes,
InternalModuleDeclaration,
ModuleJoinerConfig,
FindConfig,
FilterableApiKeyProps,
} from "@medusajs/types"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { ApiKey } from "@models"
import {
CreateApiKeyDTO,
RevokeApiKeyInput,
TokenDTO,
UpdateApiKeyInput,
} from "@types"
import {
ApiKeyType,
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
isObject,
isString,
promiseAll,
} from "@medusajs/utils"
const scrypt = util.promisify(crypto.scrypt)
const generateMethodForModels = []
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
apiKeyService: ModulesSdkTypes.InternalModuleService<any>
}
export default class ApiKeyModuleService<TEntity extends ApiKey = ApiKey>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
ApiKeyTypes.ApiKeyDTO,
{
ApiKey: { dto: ApiKeyTypes.ApiKeyDTO }
}
>(ApiKey, generateMethodForModels, entityNameToLinkableKeysMap)
implements IApiKeyModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly apiKeyService_: ModulesSdkTypes.InternalModuleService<TEntity>
constructor(
{ baseRepository, apiKeyService }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.apiKeyService_ = apiKeyService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
create(
data: ApiKeyTypes.CreateApiKeyDTO[],
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
create(
data: ApiKeyTypes.CreateApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager("baseRepository_")
async create(
data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const [createdApiKeys, generatedTokens] = await this.create_(
Array.isArray(data) ? data : [data],
sharedContext
)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(createdApiKeys, {
populate: true,
})
// When creating we want to return the raw token, as this will be the only time the user will be able to take note of it for future use.
const responseWithRawToken = serializedResponse.map((key) => ({
...key,
token:
generatedTokens.find((t) => t.hashedToken === key.token)?.rawToken ??
key.token,
salt: undefined,
}))
return Array.isArray(data) ? responseWithRawToken : responseWithRawToken[0]
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data: ApiKeyTypes.CreateApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], TokenDTO[]]> {
await this.validateCreateApiKeys_(data, sharedContext)
const normalizedInput: CreateApiKeyDTO[] = []
const generatedTokens: TokenDTO[] = []
for (const key of data) {
let tokenData: TokenDTO
if (key.type === ApiKeyType.PUBLISHABLE) {
tokenData = ApiKeyModuleService.generatePublishableKey()
} else {
tokenData = await ApiKeyModuleService.generateSecretKey()
}
generatedTokens.push(tokenData)
normalizedInput.push({
...key,
token: tokenData.hashedToken,
salt: tokenData.salt,
redacted: tokenData.redacted,
})
}
const createdApiKeys = await this.apiKeyService_.create(
normalizedInput,
sharedContext
)
return [createdApiKeys, generatedTokens]
}
async upsert(
data: ApiKeyTypes.UpsertApiKeyDTO[],
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
async upsert(
data: ApiKeyTypes.UpsertApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager("baseRepository_")
async upsert(
data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(apiKey): apiKey is UpdateApiKeyInput => !!apiKey.id
)
const forCreate = input.filter(
(apiKey): apiKey is ApiKeyTypes.CreateApiKeyDTO => !apiKey.id
)
const operations: Promise<ApiKeyTypes.ApiKeyDTO[]>[] = []
if (forCreate.length) {
const op = async () => {
const [createdApiKeys, generatedTokens] = await this.create_(
forCreate,
sharedContext
)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(createdApiKeys, {
populate: true,
})
return serializedResponse.map(
(key) =>
({
...key,
token:
generatedTokens.find((t) => t.hashedToken === key.token)
?.rawToken ?? key.token,
salt: undefined,
} as ApiKeyTypes.ApiKeyDTO)
)
}
operations.push(op())
}
if (forUpdate.length) {
const op = async () => {
const updateResp = await this.update_(forUpdate, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
updateResp
)
}
operations.push(op())
}
const result = (await promiseAll(operations)).flat()
return Array.isArray(data) ? result : result[0]
}
async update(
id: string,
data: ApiKeyTypes.UpdateApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
async update(
selector: FilterableApiKeyProps,
data: ApiKeyTypes.UpdateApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_")
async update(
idOrSelector: string | FilterableApiKeyProps,
data: ApiKeyTypes.UpdateApiKeyDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
let normalizedInput = await this.normalizeUpdateInput_<UpdateApiKeyInput>(
idOrSelector,
data,
sharedContext
)
const updatedApiKeys = await this.update_(normalizedInput, sharedContext)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(updatedApiKeys.map(omitToken), {
populate: true,
})
return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
}
@InjectTransactionManager("baseRepository_")
protected async update_(
normalizedInput: UpdateApiKeyInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const updateRequest = normalizedInput.map((k) => ({
id: k.id,
title: k.title,
}))
const updatedApiKeys = await this.apiKeyService_.update(
updateRequest,
sharedContext
)
return updatedApiKeys
}
@InjectManager("baseRepository_")
async retrieve(
id: string,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO> {
const apiKey = await this.apiKeyService_.retrieve(id, config, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO>(
omitToken(apiKey),
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async list(
filters?: ApiKeyTypes.FilterableApiKeyProps,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> {
const apiKeys = await this.apiKeyService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
apiKeys.map(omitToken),
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCount(
filters?: ApiKeyTypes.FilterableApiKeyProps,
config?: FindConfig<ApiKeyTypes.ApiKeyDTO>,
sharedContext?: Context
): Promise<[ApiKeyTypes.ApiKeyDTO[], number]> {
const [apiKeys, count] = await this.apiKeyService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
apiKeys.map(omitToken),
{
populate: true,
}
),
count,
]
}
async revoke(
id: string,
data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO>
async revoke(
selector: FilterableApiKeyProps,
data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager("baseRepository_")
async revoke(
idOrSelector: string | FilterableApiKeyProps,
data: ApiKeyTypes.RevokeApiKeyDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO[] | ApiKeyTypes.ApiKeyDTO> {
const normalizedInput = await this.normalizeUpdateInput_<RevokeApiKeyInput>(
idOrSelector,
data,
sharedContext
)
const revokedApiKeys = await this.revoke_(normalizedInput, sharedContext)
const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[]
>(revokedApiKeys.map(omitToken), {
populate: true,
})
return isString(idOrSelector) ? serializedResponse[0] : serializedResponse
}
@InjectTransactionManager("baseRepository_")
async revoke_(
normalizedInput: RevokeApiKeyInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
await this.validateRevokeApiKeys_(normalizedInput)
const updateRequest = normalizedInput.map((k) => {
const revokedAt = new Date()
if (k.revoke_in && k.revoke_in > 0) {
revokedAt.setSeconds(revokedAt.getSeconds() + k.revoke_in)
}
return {
id: k.id,
revoked_at: revokedAt,
revoked_by: k.revoked_by,
}
})
const revokedApiKeys = await this.apiKeyService_.update(
updateRequest,
sharedContext
)
return revokedApiKeys
}
@InjectManager("baseRepository_")
async authenticate(
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | false> {
const result = await this.authenticate_(token, sharedContext)
if (!result) {
return false
}
const serialized =
await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO>(result, {
populate: true,
})
return serialized
}
@InjectTransactionManager("baseRepository_")
protected async authenticate_(
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKey | false> {
// Since we only allow up to 2 active tokens, getitng the list and checking each token isn't an issue.
// We can always filter on the redacted key if we add support for an arbitrary number of tokens.
const secretKeys = await this.apiKeyService_.list(
{
type: ApiKeyType.SECRET,
// If the revoke date is set in the future, it means the key is still valid.
$or: [
{ revoked_at: { $eq: null } },
{ revoked_at: { $gt: new Date() } },
],
},
{ take: null },
sharedContext
)
const matches = await promiseAll(
secretKeys.map(async (dbKey) => {
const hashedInput = await ApiKeyModuleService.calculateHash(
token,
dbKey.salt
)
if (hashedInput === dbKey.token) {
return dbKey
}
return undefined
})
)
const matchedKeys = matches.filter((match) => !!match)
if (!matchedKeys.length) {
return false
}
return matchedKeys[0]!
}
protected async validateCreateApiKeys_(
data: ApiKeyTypes.CreateApiKeyDTO[],
sharedContext: Context = {}
): Promise<void> {
if (!data.length) {
return
}
// There can only be 2 secret keys at most, and one has to be with a revoked_at date set, so only 1 can be newly created.
const secretKeysToCreate = data.filter((k) => k.type === ApiKeyType.SECRET)
if (!secretKeysToCreate.length) {
return
}
if (secretKeysToCreate.length > 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You can only create one secret key at a time. You tried to create ${secretKeysToCreate.length} secret keys.`
)
}
// There already is a key that is not set to expire/or it hasn't expired
const dbSecretKeys = await this.apiKeyService_.list(
{
type: ApiKeyType.SECRET,
$or: [
{ revoked_at: { $eq: null } },
{ revoked_at: { $gt: new Date() } },
],
},
{ take: null },
sharedContext
)
if (dbSecretKeys.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You can only have one active secret key a time. Revoke or delete your existing key before creating a new one.`
)
}
}
protected async normalizeUpdateInput_<T>(
idOrSelector: string | FilterableApiKeyProps,
data: Omit<T, "id">,
sharedContext: Context = {}
): Promise<T[]> {
let normalizedInput: T[] = []
if (isString(idOrSelector)) {
normalizedInput = [{ id: idOrSelector, ...data } as T]
}
if (isObject(idOrSelector)) {
const apiKeys = await this.apiKeyService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = apiKeys.map(
(apiKey) =>
({
id: apiKey.id,
...data,
} as T)
)
}
return normalizedInput
}
protected async validateRevokeApiKeys_(
data: RevokeApiKeyInput[],
sharedContext: Context = {}
): Promise<void> {
if (!data.length) {
return
}
if (data.some((k) => !k.id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You must provide an api key id field when revoking a key.`
)
}
if (data.some((k) => !k.revoked_by)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You must provide a revoked_by field when revoking a key.`
)
}
const revokedApiKeys = await this.apiKeyService_.list(
{
id: data.map((k) => k.id),
type: ApiKeyType.SECRET,
revoked_at: { $ne: null },
},
{},
sharedContext
)
if (revokedApiKeys.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`There are ${revokedApiKeys.length} secret keys that are already revoked.`
)
}
}
// These are public keys, so there is no point hashing them.
protected static generatePublishableKey(): TokenDTO {
const token = "pk_" + crypto.randomBytes(32).toString("hex")
return {
rawToken: token,
hashedToken: token,
salt: "",
redacted: redactKey(token),
}
}
protected static async generateSecretKey(): Promise<TokenDTO> {
const token = "sk_" + crypto.randomBytes(32).toString("hex")
const salt = crypto.randomBytes(16).toString("hex")
const hashed = await this.calculateHash(token, salt)
return {
rawToken: token,
hashedToken: hashed,
salt,
redacted: redactKey(token),
}
}
protected static async calculateHash(
token: string,
salt: string
): Promise<string> {
return ((await scrypt(token, salt, 64)) as Buffer).toString("hex")
}
}
// We are mutating the object here as what microORM relies on non-enumerable fields for serialization, among other things.
const omitToken = (
// We have to make salt optional before deleting it (and we do want it required in the DB)
key: Omit<ApiKey, "salt"> & { salt?: string }
): Omit<ApiKey, "salt"> => {
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
delete key.salt
return key
}
const redactKey = (key: string): string => {
return [key.slice(0, 6), key.slice(-3)].join("***")
}

View File

@@ -0,0 +1 @@
export { default as ApiKeyModuleService } from "./api-key-module-service"

View File

@@ -0,0 +1,26 @@
import { ApiKeyType, RevokeApiKeyDTO, UpdateApiKeyDTO } from "@medusajs/types"
import { IEventBusModuleService, Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
}
export type CreateApiKeyDTO = {
token: string
salt: string
redacted: string
title: string
type: ApiKeyType
created_by: string
}
export type TokenDTO = {
rawToken: string
hashedToken: string
salt: string
redacted: string
}
export type UpdateApiKeyInput = UpdateApiKeyDTO & { id: string }
export type RevokeApiKeyInput = RevokeApiKeyDTO & { id: string }

View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"lib": ["es2020"],
"target": "es2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": false,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true, // to use ES5 specific tooling
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@models": ["./src/models"],
"@services": ["./src/services"],
"@repositories": ["./src/repositories"],
"@types": ["./src/types"]
}
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}

6
packages/modules/auth/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,25 @@
# @medusajs/auth
## 0.0.3
### Patch Changes
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`1fd0457c15`](https://github.com/medusajs/medusa/commit/1fd0457c153b2ef7657c052878d8e5364e1b324a), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`d4b921f3db`](https://github.com/medusajs/medusa/commit/d4b921f3dbe0a38f1565a8de759996c70798d58e), [`ac86362e81`](https://github.com/medusajs/medusa/commit/ac86362e81d8523cb8e3dfad026fc94658513018), [`e4acde1aa2`](https://github.com/medusajs/medusa/commit/e4acde1aa2eb57f07e6692fe8b61f728948b9a96), [`1a661adf3e`](https://github.com/medusajs/medusa/commit/1a661adf3ef4991aa6e237dd894b6a5c47cd4aca), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`04a532e5ef`](https://github.com/medusajs/medusa/commit/04a532e5efabbf75b1e4155520b1da175b686ffc), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`ac829fc67f`](https://github.com/medusajs/medusa/commit/ac829fc67f7495b08f28e55923c59f0fd6320311), [`d9d5afc3cf`](https://github.com/medusajs/medusa/commit/d9d5afc3cfc29221d0e65bff7b78474a8fb8f31f), [`c3c4f49fc2`](https://github.com/medusajs/medusa/commit/c3c4f49fc2126f950e69e291ca939ca88a15afd3), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`0d46abf0ff`](https://github.com/medusajs/medusa/commit/0d46abf0ffa4c5e03bf7d2a9cdf1db828a76bea8), [`fafde4f54d`](https://github.com/medusajs/medusa/commit/fafde4f54d3ef75a7d382e6cbf94e38b3deae99b), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`0c705d7bd4`](https://github.com/medusajs/medusa/commit/0c705d7bd41a768c48017ae95b3c8414d96c6acb), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`1d91b7429b`](https://github.com/medusajs/medusa/commit/1d91b7429beebd6f09d5027f7f7e1fe74ce3a8ff), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`1ed5f918c3`](https://github.com/medusajs/medusa/commit/1ed5f918c31794a70aca4a4e4cd83cf456593baa), [`c20eb15cd9`](https://github.com/medusajs/medusa/commit/c20eb15cd9b1bd90c8d01f68eca6f0f181cd902d), [`e5945479e0`](https://github.com/medusajs/medusa/commit/e5945479e091d9560ae3e7240306a31031ef4584), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4d51f095b3`](https://github.com/medusajs/medusa/commit/4d51f095b3f98f468cefb760512563f7b77bb9cf), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/types@1.11.14
- @medusajs/utils@1.11.7
- @medusajs/modules-sdk@1.12.9
## 0.0.2
### Patch Changes
- [#6062](https://github.com/medusajs/medusa/pull/6062) [`72bc52231c`](https://github.com/medusajs/medusa/commit/72bc52231ca3a72fa6d197a248fe07a938ed0d85) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(utils): Update base repository to infer primary keys and support composite
- [#6035](https://github.com/medusajs/medusa/pull/6035) [`b6ac768698`](https://github.com/medusajs/medusa/commit/b6ac768698a3b49d0162cb49e628386f3352d034) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Attempt to abstract the modules repository
- Updated dependencies [[`68ddd866a5`](https://github.com/medusajs/medusa/commit/68ddd866a5ff9414e2db5b80d75acc5e81948540), [`72bc52231c`](https://github.com/medusajs/medusa/commit/72bc52231ca3a72fa6d197a248fe07a938ed0d85), [`99045848f`](https://github.com/medusajs/medusa/commit/99045848fd3e863359c7878d9bc05271ed083a0e), [`af7af7374`](https://github.com/medusajs/medusa/commit/af7af737455daa0f330840a9678e6339e519dfe6), [`fc6b1772a7`](https://github.com/medusajs/medusa/commit/fc6b1772a71582bb48602c5cac7b2297e9d267a9), [`a9b4214503`](https://github.com/medusajs/medusa/commit/a9b42145032ee88aa922a11fe03e777b140c68f4), [`d85fee42e`](https://github.com/medusajs/medusa/commit/d85fee42ee7f661310584dfee5741d6c53b989bb), [`5e655dd59`](https://github.com/medusajs/medusa/commit/5e655dd59bda4ffface28db38021ba71cae6de10), [`b132ff7669`](https://github.com/medusajs/medusa/commit/b132ff76693148b3a06373c168e8dd5e02970757), [`e28fa7fbdf`](https://github.com/medusajs/medusa/commit/e28fa7fbdf45c5b1fa19848db731132a0bf1757d), [`a12c28b7d5`](https://github.com/medusajs/medusa/commit/a12c28b7d5faed733bebbb4963dff50b9c8a33bc), [`b782d3bcb7`](https://github.com/medusajs/medusa/commit/b782d3bcb7e8088a962584b9a55200dd29c2161c), [`2b9f98895e`](https://github.com/medusajs/medusa/commit/2b9f98895eaca255e01278674b11cd7cb69b388f), [`7f7cb2a263`](https://github.com/medusajs/medusa/commit/7f7cb2a263c26baf540b05a40ab3732ffeb0c73c), [`302323916`](https://github.com/medusajs/medusa/commit/302323916b6d8eaf571cd59b5fc92a913af207de), [`da5cc4cf7`](https://github.com/medusajs/medusa/commit/da5cc4cf7f7f0ef40d409704a95b025ce95477f4), [`daecd82a7`](https://github.com/medusajs/medusa/commit/daecd82a7cdf7315599f464999690414c20d6748), [`ce81cade88`](https://github.com/medusajs/medusa/commit/ce81cade887659cefe9638e3c1c2807378191c62), [`fd78f5e24`](https://github.com/medusajs/medusa/commit/fd78f5e24263f5e158c3b7d11fbf0a4436e9c17a), [`192bc336cc`](https://github.com/medusajs/medusa/commit/192bc336cc2b6ec3820d94524c046dcd3c4ac7d9), [`06b33a9b4`](https://github.com/medusajs/medusa/commit/06b33a9b4525b77b1b14b35b973209700945654e), [`b6ac768698`](https://github.com/medusajs/medusa/commit/b6ac768698a3b49d0162cb49e628386f3352d034), [`130c641e5c`](https://github.com/medusajs/medusa/commit/130c641e5c91cf831de64fb87aebbfdc4d23530d), [`fade8ea7bf`](https://github.com/medusajs/medusa/commit/fade8ea7bf560343ecbde116d226ac44053cdb8e), [`8472460f53`](https://github.com/medusajs/medusa/commit/8472460f533322cc4535199aa768ac163021bc79), [`68d8daccd`](https://github.com/medusajs/medusa/commit/68d8daccd2a8508a13e211130e49017198b51fab)]:
- @medusajs/types@1.11.11
- @medusajs/utils@1.11.4
- @medusajs/modules-sdk@1.12.7

View File

@@ -0,0 +1,3 @@
# Auth Module
The Auth Module is Medusas authentication engine engine. It provides functions to authenticate users through identity providers and store metadata about users that can be used for authorization purposes.

View File

@@ -0,0 +1,37 @@
import { AuthUser } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
export async function createAuthUsers(
manager: SqlEntityManager,
userData: any[] = [
{
id: "test-id",
entity_id: "test-id",
provider: "manual",
scope: "store",
},
{
id: "test-id-1",
entity_id: "test-id-1",
provider: "manual",
scope: "store",
},
{
entity_id: "test-id-2",
provider: "store",
scope: "store",
},
]
): Promise<AuthUser[]> {
const authUsers: AuthUser[] = []
for (const user of userData) {
const authUser = manager.create(AuthUser, user)
authUsers.push(authUser)
}
await manager.persistAndFlush(authUsers)
return authUsers
}

View File

@@ -0,0 +1,231 @@
import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import { Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
jest.setTimeout(30000)
moduleIntegrationTestRunner({
moduleName: Modules.AUTH,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IAuthModuleService>) => {
describe("AuthUser Service", () => {
beforeEach(async () => {
await createAuthUsers(MikroOrmWrapper.forkManager())
})
describe("list", () => {
it("should list authUsers", async () => {
const authUsers = await service.list()
const serialized = JSON.parse(JSON.stringify(authUsers))
expect(serialized).toEqual([
expect.objectContaining({
provider: "store",
}),
expect.objectContaining({
provider: "manual",
}),
expect.objectContaining({
provider: "manual",
}),
])
})
it("should list authUsers by id", async () => {
const authUsers = await service.list({
id: ["test-id"],
})
expect(authUsers).toEqual([
expect.objectContaining({
id: "test-id",
}),
])
})
it("should list authUsers by provider_id", async () => {
const authUsers = await service.list({
provider: "manual",
})
const serialized = JSON.parse(JSON.stringify(authUsers))
expect(serialized).toEqual([
expect.objectContaining({
id: "test-id",
}),
expect.objectContaining({
id: "test-id-1",
}),
])
})
})
describe("listAndCount", () => {
it("should list authUsers", async () => {
const [authUsers, count] = await service.listAndCount()
const serialized = JSON.parse(JSON.stringify(authUsers))
expect(count).toEqual(3)
expect(serialized).toEqual([
expect.objectContaining({
provider: "store",
}),
expect.objectContaining({
provider: "manual",
}),
expect.objectContaining({
provider: "manual",
}),
])
})
it("should listAndCount authUsers by provider_id", async () => {
const [authUsers, count] = await service.listAndCount({
provider: "manual",
})
expect(count).toEqual(2)
expect(authUsers).toEqual([
expect.objectContaining({
id: "test-id",
}),
expect.objectContaining({
id: "test-id-1",
}),
])
})
})
describe("retrieve", () => {
const id = "test-id"
it("should return an authUser for the given id", async () => {
const authUser = await service.retrieve(id)
expect(authUser).toEqual(
expect.objectContaining({
id,
})
)
})
it("should return authUser based on config select param", async () => {
const authUser = await service.retrieve(id, {
select: ["id"],
})
const serialized = JSON.parse(JSON.stringify(authUser))
expect(serialized).toEqual({
id,
})
})
it("should throw an error when an authUser with the given id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"AuthUser with id: does-not-exist was not found"
)
})
it("should throw an error when a authUserId is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual("authUser - id must be defined")
})
})
describe("delete", () => {
it("should delete the authUsers given an id successfully", async () => {
const id = "test-id"
await service.delete([id])
const authUsers = await service.list({
id: [id],
})
expect(authUsers).toHaveLength(0)
})
})
describe("update", () => {
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'AuthUser with id "does-not-exist" not found'
)
})
it("should update authUser", async () => {
const id = "test-id"
await service.update([
{
id,
provider_metadata: { email: "test@email.com" },
},
])
const [authUser] = await service.list({ id: [id] })
expect(authUser).toEqual(
expect.objectContaining({
provider_metadata: { email: "test@email.com" },
})
)
})
})
describe("create", () => {
it("should create a authUser successfully", async () => {
await service.create([
{
id: "test",
provider: "manual",
entity_id: "test",
scope: "store",
},
])
const [authUser] = await service.list({
id: ["test"],
})
expect(authUser).toEqual(
expect.objectContaining({
id: "test",
})
)
})
})
})
},
})

View File

@@ -0,0 +1,237 @@
import { IAuthModuleService } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
jest.setTimeout(30000)
moduleIntegrationTestRunner({
moduleName: Modules.AUTH,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IAuthModuleService>) => {
describe("AuthModuleService - AuthUser", () => {
beforeEach(async () => {
await createAuthUsers(MikroOrmWrapper.forkManager())
})
describe("listAuthUsers", () => {
it("should list authUsers", async () => {
const authUsers = await service.list()
expect(authUsers).toEqual([
expect.objectContaining({
provider: "store",
}),
expect.objectContaining({
provider: "manual",
}),
expect.objectContaining({
provider: "manual",
}),
])
})
it("should list authUsers by id", async () => {
const authUsers = await service.list({
id: ["test-id"],
})
expect(authUsers).toEqual([
expect.objectContaining({
id: "test-id",
}),
])
})
it("should list authUsers by provider", async () => {
const authUsers = await service.list({
provider: "manual",
})
expect(authUsers).toEqual([
expect.objectContaining({
id: "test-id",
}),
expect.objectContaining({
id: "test-id-1",
}),
])
})
})
describe("listAndCountAuthUsers", () => {
it("should list and count authUsers", async () => {
const [authUsers, count] = await service.listAndCount()
expect(count).toEqual(3)
expect(authUsers).toEqual([
expect.objectContaining({
provider: "store",
}),
expect.objectContaining({
provider: "manual",
}),
expect.objectContaining({
provider: "manual",
}),
])
})
it("should listAndCount authUsers by provider_id", async () => {
const [authUsers, count] = await service.listAndCount({
provider: "manual",
})
expect(count).toEqual(2)
expect(authUsers).toEqual([
expect.objectContaining({
id: "test-id",
}),
expect.objectContaining({
id: "test-id-1",
}),
])
})
})
describe("retrieveAuthUser", () => {
const id = "test-id"
it("should return an authUser for the given id", async () => {
const authUser = await service.retrieve(id)
expect(authUser).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when an authUser with the given id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"AuthUser with id: does-not-exist was not found"
)
})
it("should not return an authUser with password hash", async () => {
const authUser = await service.retrieve("test-id-1")
expect(authUser).toEqual(
expect.objectContaining({
id: "test-id-1",
})
)
expect(authUser["password_hash"]).toEqual(undefined)
})
it("should throw an error when a authUserId is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual("authUser - id must be defined")
})
it("should return authUser based on config select param", async () => {
const authUser = await service.retrieve(id, {
select: ["id"],
})
expect(authUser).toEqual({
id,
})
})
})
describe("deleteAuthUser", () => {
const id = "test-id"
it("should delete the authUsers given an id successfully", async () => {
await service.delete([id])
const authUsers = await service.list({
id: [id],
})
expect(authUsers).toHaveLength(0)
})
})
describe("updateAuthUser", () => {
const id = "test-id"
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'AuthUser with id "does-not-exist" not found'
)
})
it("should update authUser", async () => {
await service.update([
{
id,
provider_metadata: { email: "test@email.com" },
},
])
const [authUser] = await service.list({ id: [id] })
expect(authUser).toEqual(
expect.objectContaining({
provider_metadata: { email: "test@email.com" },
})
)
})
})
describe("createAuthUser", () => {
it("should create a authUser successfully", async () => {
await service.create([
{
id: "test",
provider: "manual",
entity_id: "test",
scope: "store",
},
])
const [authUser, count] = await service.listAndCount({
id: ["test"],
})
expect(count).toEqual(1)
expect(authUser[0]).toEqual(
expect.objectContaining({
id: "test",
})
)
})
})
})
},
})

View File

@@ -0,0 +1,41 @@
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
jest.setTimeout(30000)
moduleIntegrationTestRunner({
moduleName: Modules.AUTH,
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IAuthModuleService>) => {
describe("AuthModuleService - AuthProvider", () => {
describe("authenticate", () => {
it("authenticate validates that a provider is registered in container", async () => {
const { success, error } = await service.authenticate(
"notRegistered",
{} as any
)
expect(success).toBe(false)
expect(error).toEqual(
"AuthenticationProvider: notRegistered wasn't registered in the module. Have you configured your options correctly?"
)
})
it("fails to authenticate using a valid provider with an invalid scope", async () => {
const { success, error } = await service.authenticate("emailpass", {
authScope: "non-existing",
} as any)
expect(success).toBe(false)
expect(error).toEqual(
`Scope "non-existing" is not valid for provider emailpass`
)
})
})
})
},
})

View File

@@ -0,0 +1,133 @@
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
import Scrypt from "scrypt-kdf"
import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
jest.setTimeout(30000)
const seedDefaultData = async (manager) => {
await createAuthUsers(manager)
}
moduleIntegrationTestRunner({
moduleName: Modules.AUTH,
moduleOptions: {
providers: [
{
name: "emailpass",
scopes: {
admin: {},
store: {},
},
},
],
},
testSuite: ({
MikroOrmWrapper,
service,
}: SuiteOptions<IAuthModuleService>) => {
describe("AuthModuleService - AuthProvider", () => {
describe("authenticate", () => {
it("authenticate validates that a provider is registered in container", async () => {
const password = "supersecret"
const email = "test@test.com"
const passwordHash = (
await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 })
).toString("base64")
await seedDefaultData(MikroOrmWrapper.forkManager())
await createAuthUsers(MikroOrmWrapper.forkManager(), [
// Add authenticated user
{
provider: "emailpass",
entity_id: email,
scope: "store",
provider_metadata: {
password: passwordHash,
},
},
])
const res = await service.authenticate("emailpass", {
body: {
email: "test@test.com",
password: password,
},
authScope: "store",
} as any)
expect(res).toEqual({
success: true,
authUser: expect.objectContaining({
entity_id: email,
provider_metadata: {},
}),
})
})
it("fails when no password is given", async () => {
await seedDefaultData(MikroOrmWrapper.forkManager())
const res = await service.authenticate("emailpass", {
body: { email: "test@test.com" },
authScope: "store",
} as any)
expect(res).toEqual({
success: false,
error: "Password should be a string",
})
})
it("fails when no email is given", async () => {
await seedDefaultData(MikroOrmWrapper.forkManager())
const res = await service.authenticate("emailpass", {
body: { password: "supersecret" },
authScope: "store",
} as any)
expect(res).toEqual({
success: false,
error: "Email should be a string",
})
})
it("fails with an invalid password", async () => {
const password = "supersecret"
const email = "test@test.com"
const passwordHash = (
await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 })
).toString("base64")
await seedDefaultData(MikroOrmWrapper.forkManager())
await createAuthUsers(MikroOrmWrapper.forkManager(), [
// Add authenticated user
{
provider: "emailpass",
scope: "store",
entity_id: email,
provider_metadata: {
password_hash: passwordHash,
},
},
])
const res = await service.authenticate("emailpass", {
body: {
email: "test@test.com",
password: "password",
},
authScope: "store",
} as any)
expect(res).toEqual({
success: false,
error: "Invalid email or password",
})
})
})
})
},
})

View File

@@ -0,0 +1,21 @@
module.exports = {
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
"^@providers": "<rootDir>/src/providers",
},
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.spec.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
}

View File

@@ -0,0 +1,8 @@
import * as entities from "./src/models"
module.exports = {
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-auth",
type: "postgresql",
}

View File

@@ -0,0 +1,64 @@
{
"name": "@medusajs/auth",
"version": "0.0.3",
"description": "Medusa Auth module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"bin": {
"medusa-auth-seed": "dist/scripts/bin/run-seed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/auth"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
"test": "jest --runInBand --bail --passWithNoTests --forceExit -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
},
"devDependencies": {
"@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"medusa-test-utils": "^1.1.42",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.9",
"@medusajs/types": "^1.11.14",
"@medusajs/utils": "^1.11.7",
"@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"dotenv": "^16.4.5",
"jsonwebtoken": "^9.0.2",
"knex": "2.4.2",
"scrypt-kdf": "^2.0.1",
"simple-oauth2": "^5.0.0"
}
}

View File

@@ -0,0 +1,11 @@
import {
moduleDefinition,
revertMigration,
runMigrations,
} from "./module-definition"
export default moduleDefinition
export { revertMigration, runMigrations }
export * from "./initialize"
export * from "./loaders"

View File

@@ -0,0 +1,31 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
Modules,
} from "@medusajs/modules-sdk"
import { IAuthModuleService, ModulesSdkTypes } from "@medusajs/types"
import { InitializeModuleInjectableDependencies } from "@types"
import { moduleDefinition } from "../module-definition"
export const initialize = async (
options?:
| ModulesSdkTypes.ModuleBootstrapDeclaration
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<IAuthModuleService> => {
const loaded = await MedusaModule.bootstrap<IAuthModuleService>({
moduleKey: Modules.AUTH,
defaultPath: MODULE_PACKAGE_NAMES[Modules.AUTH],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration, // TODO: Add provider configuration
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.AUTH]
}

View File

@@ -0,0 +1,31 @@
import { AuthUser } from "@models"
import { MapToConfig } from "@medusajs/utils"
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
export const LinkableKeys = {
auth_user_id: AuthUser.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.AUTH,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: {
name: ["auth_user", "auth_users"],
args: {
entity: AuthUser.name,
},
},
}

View File

@@ -0,0 +1,38 @@
import * as AuthModels from "../models"
import {
InternalModuleDeclaration,
LoaderOptions,
Modules,
} from "@medusajs/modules-sdk"
import { EntitySchema } from "@mikro-orm/core"
import { ModulesSdkTypes } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils"
export default async (
{
options,
container,
logger,
}: LoaderOptions<
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>,
moduleDeclaration?: InternalModuleDeclaration
): Promise<void> => {
const entities = Object.values(
AuthModels
) as unknown as EntitySchema[]
const pathToMigrations = __dirname + "/../migrations"
await ModulesSdkUtils.mikroOrmConnectionLoader({
moduleName: Modules.AUTH,
entities,
container,
options,
moduleDeclaration,
logger,
pathToMigrations,
})
}

View File

@@ -0,0 +1,10 @@
import { ModulesSdkUtils } from "@medusajs/utils"
import * as ModuleModels from "@models"
import * as ModuleRepositories from "@repositories"
import * as ModuleServices from "@services"
export default ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: ModuleModels,
moduleRepositories: ModuleRepositories,
moduleServices: ModuleServices,
})

View File

@@ -0,0 +1,3 @@
export * from "./connection"
export * from "./container"
export * from "./providers"

View File

@@ -0,0 +1,72 @@
import * as defaultProviders from "@providers"
import {
AuthModuleProviderConfig,
AuthProviderScope,
LoaderOptions,
ModulesSdkTypes,
} from "@medusajs/types"
import {
AwilixContainer,
ClassOrFunctionReturning,
Constructor,
Resolver,
asClass,
} from "awilix"
type AuthModuleProviders = {
providers: AuthModuleProviderConfig[]
}
export default async ({
container,
options,
}: LoaderOptions<
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) &
AuthModuleProviders
>): Promise<void> => {
const providerMap = new Map(
options?.providers?.map((provider) => [provider.name, provider.scopes]) ??
[]
)
// if(options?.providers?.length) {
// TODO: implement plugin provider registration
// }
const providersToLoad = Object.values(defaultProviders)
for (const provider of providersToLoad) {
container.register({
[`auth_provider_${provider.PROVIDER}`]: asClass(
provider as Constructor<any>
)
.singleton()
.inject(() => ({ scopes: providerMap.get(provider.PROVIDER) ?? {} })),
})
}
container.register({
[`auth_providers`]: asArray(providersToLoad, providerMap),
})
}
function asArray(
resolvers: (ClassOrFunctionReturning<unknown> | Resolver<unknown>)[],
providerScopeMap: Map<string, Record<string, AuthProviderScope>>
): { resolve: (container: AwilixContainer) => unknown[] } {
return {
resolve: (container: AwilixContainer) =>
resolvers.map((resolver) =>
asClass(resolver as Constructor<any>)
.inject(() => ({
// @ts-ignore
scopes: providerScopeMap.get(resolver.PROVIDER) ?? {},
}))
.resolve(container)
),
}
}

View File

@@ -0,0 +1,101 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"entity_id": {
"name": "entity_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"provider": {
"name": "provider",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"scope": {
"name": "scope",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"user_metadata": {
"name": "user_metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"app_metadata": {
"name": "app_metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "json"
},
"provider_metadata": {
"name": "provider_metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "auth_user",
"schema": "public",
"indexes": [
{
"keyName": "IDX_auth_user_provider_scope_entity_id",
"columnNames": [
"provider",
"scope",
"entity_id"
],
"composite": true,
"primary": false,
"unique": true
},
{
"keyName": "auth_user_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
}
]
}

View File

@@ -0,0 +1,16 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20240205025924 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider" text not null, "scope" text not null, "user_metadata" jsonb null, "app_metadata" jsonb not null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'
)
this.addSql(
'alter table "auth_user" add constraint "IDX_auth_user_provider_scope_entity_id" unique ("provider", "scope", "entity_id");'
)
}
async down(): Promise<void> {
this.addSql('drop table if exists "auth_user" cascade;')
}
}

View File

@@ -0,0 +1,53 @@
import {
BeforeCreate,
Entity,
OnInit,
OptionalProps,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils"
type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata"
@Entity()
@Unique({
properties: ["provider", "scope", "entity_id"],
name: "IDX_auth_user_provider_scope_entity_id",
})
export default class AuthUser {
[OptionalProps]: OptionalFields
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
entity_id: string
@Property({ columnType: "text" })
provider: string
@Property({ columnType: "text" })
scope: string
@Property({ columnType: "jsonb", nullable: true })
user_metadata: Record<string, unknown> | null
@Property({ columnType: "jsonb" })
app_metadata: Record<string, unknown> = {}
@Property({ columnType: "jsonb", nullable: true })
provider_metadata: Record<string, unknown> | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "authusr")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "authusr")
}
}

View File

@@ -0,0 +1 @@
export { default as AuthUser } from "./auth-user"

View File

@@ -0,0 +1,32 @@
import * as Models from "@models"
import { AuthModuleService } from "@services"
import { ModuleExports } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
import { ModulesSdkUtils } from "@medusajs/utils"
import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container"
import loadProviders from "./loaders/providers"
const migrationScriptOptions = {
moduleName: Modules.AUTH,
models: Models,
pathToMigrations: __dirname + "/migrations",
}
export const runMigrations = ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
const service = AuthModuleService
const loaders = [loadContainer, loadConnection, loadProviders] as any
export const moduleDefinition: ModuleExports = {
service,
loaders,
runMigrations,
revertMigration,
}

View File

@@ -0,0 +1,106 @@
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import {
AbstractAuthModuleProvider,
MedusaError,
isString,
} from "@medusajs/utils"
import { AuthUserService } from "@services"
import Scrypt from "scrypt-kdf"
class EmailPasswordProvider extends AbstractAuthModuleProvider {
public static PROVIDER = "emailpass"
public static DISPLAY_NAME = "Email/Password Authentication"
protected readonly authUserSerivce_: AuthUserService
constructor({ authUserService }: { authUserService: AuthUserService }) {
super(arguments[0], {
provider: EmailPasswordProvider.PROVIDER,
displayName: EmailPasswordProvider.DISPLAY_NAME,
})
this.authUserSerivce_ = authUserService
}
private getHashConfig() {
const scopeConfig = this.scopeConfig_.hashConfig as
| Scrypt.ScryptParams
| undefined
const defaultHashConfig = { logN: 15, r: 8, p: 1 }
// Return custom defined hash config or default hash parameters
return scopeConfig ?? defaultHashConfig
}
async authenticate(
userData: AuthenticationInput
): Promise<AuthenticationResponse> {
const { email, password } = userData.body
if (!password || !isString(password)) {
return {
success: false,
error: "Password should be a string",
}
}
if (!email || !isString(email)) {
return {
success: false,
error: "Email should be a string",
}
}
let authUser
try {
authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId(
email,
EmailPasswordProvider.PROVIDER
)
} catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) {
const password_hash = await Scrypt.kdf(password, this.getHashConfig())
const [createdAuthUser] = await this.authUserSerivce_.create([
{
entity_id: email,
provider: EmailPasswordProvider.PROVIDER,
scope: this.scope_,
provider_metadata: {
password: password_hash.toString("base64"),
},
},
])
return {
success: true,
authUser: JSON.parse(JSON.stringify(createdAuthUser)),
}
}
return { success: false, error: error.message }
}
const password_hash = authUser.provider_metadata?.password
if (isString(password_hash)) {
const buf = Buffer.from(password_hash as string, "base64")
const success = await Scrypt.verify(buf, password)
if (success) {
delete authUser.provider_metadata!.password
return { success, authUser: JSON.parse(JSON.stringify(authUser)) }
}
}
return {
success: false,
error: "Invalid email or password",
}
}
}
export default EmailPasswordProvider

View File

@@ -0,0 +1,223 @@
import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types"
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
import { AuthUserService } from "@services"
import jwt, { JwtPayload } from "jsonwebtoken"
import { AuthorizationCode } from "simple-oauth2"
import url from "url"
type InjectedDependencies = {
authUserService: AuthUserService
}
type ProviderConfig = {
clientID: string
clientSecret: string
callbackURL: string
successRedirectUrl?: string
}
class GoogleProvider extends AbstractAuthModuleProvider {
public static PROVIDER = "google"
public static DISPLAY_NAME = "Google Authentication"
protected readonly authUserService_: AuthUserService
constructor({ authUserService }: InjectedDependencies) {
super(arguments[0], {
provider: GoogleProvider.PROVIDER,
displayName: GoogleProvider.DISPLAY_NAME,
})
this.authUserService_ = authUserService
}
async authenticate(
req: AuthenticationInput
): Promise<AuthenticationResponse> {
if (req.query?.error) {
return {
success: false,
error: `${req.query.error_description}, read more at: ${req.query.error_uri}`,
}
}
let config: ProviderConfig
try {
config = await this.getProviderConfig(req)
} catch (error) {
return { success: false, error: error.message }
}
return this.getRedirect(config)
}
async validateCallback(
req: AuthenticationInput
): Promise<AuthenticationResponse> {
if (req.query && req.query.error) {
return {
success: false,
error: `${req.query.error_description}, read more at: ${req.query.error_uri}`,
}
}
let config: ProviderConfig
try {
config = await this.getProviderConfig(req)
} catch (error) {
return { success: false, error: error.message }
}
const code = req.query?.code ?? req.body?.code
return await this.validateCallbackToken(code, config)
}
// abstractable
async verify_(refreshToken: string) {
const jwtData = jwt.decode(refreshToken, {
complete: true,
}) as JwtPayload
const entity_id = jwtData.payload.email
let authUser
try {
authUser = await this.authUserService_.retrieveByProviderAndEntityId(
entity_id,
GoogleProvider.PROVIDER
)
} catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) {
const [createdAuthUser] = await this.authUserService_.create([
{
entity_id,
provider: GoogleProvider.PROVIDER,
user_metadata: jwtData!.payload,
scope: this.scope_,
},
])
authUser = createdAuthUser
} else {
return { success: false, error: error.message }
}
}
return {
success: true,
authUser,
}
}
// abstractable
private async validateCallbackToken(
code: string,
{ clientID, callbackURL, clientSecret }: ProviderConfig
) {
const client = this.getAuthorizationCodeHandler({ clientID, clientSecret })
const tokenParams = {
code,
redirect_uri: callbackURL,
}
try {
const accessToken = await client.getToken(tokenParams)
const { authUser, success } = await this.verify_(
accessToken.token.id_token
)
const { successRedirectUrl } = this.getConfigFromScope()
return {
success,
authUser,
successRedirectUrl,
}
} catch (error) {
return { success: false, error: error.message }
}
}
private getConfigFromScope(): ProviderConfig {
const config: Partial<ProviderConfig> = { ...this.scopeConfig_ }
if (!config.clientID) {
throw new Error("Google clientID is required")
}
if (!config.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!config.callbackURL) {
throw new Error("Google callbackUrl is required")
}
return config as ProviderConfig
}
private originalURL(req: AuthenticationInput) {
const host = req.headers.host
const protocol = req.protocol
const path = req.url || ""
return protocol + "://" + host + path
}
private async getProviderConfig(
req: AuthenticationInput
): Promise<ProviderConfig> {
const config = this.getConfigFromScope()
const callbackURL = config.callbackURL
const parsedCallbackUrl = !url.parse(callbackURL).protocol
? url.resolve(this.originalURL(req), callbackURL)
: callbackURL
return { ...config, callbackURL: parsedCallbackUrl }
}
// Abstractable
private getRedirect({ clientID, callbackURL, clientSecret }: ProviderConfig) {
const client = this.getAuthorizationCodeHandler({ clientID, clientSecret })
const location = client.authorizeURL({
redirect_uri: callbackURL,
scope: "email profile",
})
return { success: true, location }
}
private getAuthorizationCodeHandler({
clientID,
clientSecret,
}: {
clientID: string
clientSecret: string
}) {
const config = {
client: {
id: clientID,
secret: clientSecret,
},
auth: {
// TODO: abstract to not be google specific
authorizeHost: "https://accounts.google.com",
authorizePath: "/o/oauth2/v2/auth",
tokenHost: "https://www.googleapis.com",
tokenPath: "/oauth2/v4/token",
},
}
return new AuthorizationCode(config)
}
}
export default GoogleProvider

View File

@@ -0,0 +1,2 @@
export { default as EmailPasswordProvider } from "./email-password"
export { default as GoogleProvider } from "./google"

View File

@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env node
import { EOL } from "os"
import { run } from "../seed"
const args = process.argv
const path = args.pop() as string
export default (async () => {
const { config } = await import("dotenv")
config()
if (!path) {
throw new Error(
`filePath is required.${EOL}Example: medusa-auth-seed <filePath>`
)
}
await run({ path })
})()

View File

@@ -0,0 +1,65 @@
import * as AuthModels from "@models"
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { EOL } from "os"
import { EntitySchema } from "@mikro-orm/core"
import { Modules } from "@medusajs/modules-sdk"
import { resolve } from "path"
export async function run({
options,
logger,
path,
}: Partial<
Pick<
LoaderOptions<ModulesSdkTypes.ModuleServiceInitializeOptions>,
"options" | "logger"
>
> & {
path: string
}) {
logger ??= console as unknown as Logger
logger.info(`Loading seed data from ${path}...`)
const { authenticationData } = await import(
resolve(process.cwd(), path)
).catch((e) => {
logger?.error(
`Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following: authenticationData.${EOL}${e}`
)
throw e
})
const dbData = ModulesSdkUtils.loadDatabaseConfig(
Modules.AUTH,
options
)!
const entities = Object.values(
AuthModels
) as unknown as EntitySchema[]
const pathToMigrations = __dirname + "/../migrations"
const orm = await DALUtils.mikroOrmCreateConnection(
dbData,
entities,
pathToMigrations
)
const manager = orm.em.fork()
try {
logger.info("Seeding authentication data..")
// TODO: implement authentication seed data
// await createAuthUsers(manager, authUsersData)
} catch (e) {
logger.error(
`Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}`
)
}
await orm.close(true)
}

View File

@@ -0,0 +1,156 @@
import {
AuthenticationInput,
AuthenticationResponse,
AuthTypes,
AuthUserDTO,
Context,
CreateAuthUserDTO,
DAL,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
UpdateAuthUserDTO,
} from "@medusajs/types"
import { AuthUser } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import {
AbstractAuthModuleProvider,
InjectManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
authUserService: ModulesSdkTypes.InternalModuleService<any>
}
const generateMethodForModels = [AuthUser]
export default class AuthModuleService<TAuthUser extends AuthUser = AuthUser>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
AuthTypes.AuthUserDTO,
{
AuthUser: { dto: AuthUserDTO }
}
>(AuthUser, generateMethodForModels, entityNameToLinkableKeysMap)
implements AuthTypes.IAuthModuleService
{
protected baseRepository_: DAL.RepositoryService
protected authUserService_: ModulesSdkTypes.InternalModuleService<TAuthUser>
constructor(
{ authUserService, baseRepository }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.authUserService_ = authUserService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
create(
data: CreateAuthUserDTO[],
sharedContext?: Context
): Promise<AuthUserDTO[]>
create(data: CreateAuthUserDTO, sharedContext?: Context): Promise<AuthUserDTO>
@InjectManager("baseRepository_")
async create(
data: CreateAuthUserDTO[] | CreateAuthUserDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]> {
const authUsers = await this.authUserService_.create(data, sharedContext)
return await this.baseRepository_.serialize<AuthTypes.AuthUserDTO[]>(
authUsers,
{
populate: true,
}
)
}
update(
data: UpdateAuthUserDTO[],
sharedContext?: Context
): Promise<AuthUserDTO[]>
update(data: UpdateAuthUserDTO, sharedContext?: Context): Promise<AuthUserDTO>
// TODO: should be pluralized, see convention about the methods naming or the abstract module service interface definition @engineering
@InjectManager("baseRepository_")
async update(
data: UpdateAuthUserDTO | UpdateAuthUserDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[]> {
const updatedUsers = await this.authUserService_.update(data, sharedContext)
const serializedUsers = await this.baseRepository_.serialize<
AuthTypes.AuthUserDTO[]
>(updatedUsers, {
populate: true,
})
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
}
protected getRegisteredAuthenticationProvider(
provider: string,
{ authScope }: AuthenticationInput
): AbstractAuthModuleProvider {
let containerProvider: AbstractAuthModuleProvider
try {
containerProvider = this.__container__[`auth_provider_${provider}`]
} catch (error) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`AuthenticationProvider: ${provider} wasn't registered in the module. Have you configured your options correctly?`
)
}
return containerProvider.withScope(authScope)
}
async authenticate(
provider: string,
authenticationData: AuthenticationInput
): Promise<AuthenticationResponse> {
try {
const registeredProvider = this.getRegisteredAuthenticationProvider(
provider,
authenticationData
)
return await registeredProvider.authenticate(authenticationData)
} catch (error) {
return { success: false, error: error.message }
}
}
async validateCallback(
provider: string,
authenticationData: AuthenticationInput
): Promise<AuthenticationResponse> {
try {
const registeredProvider = this.getRegisteredAuthenticationProvider(
provider,
authenticationData
)
return await registeredProvider.validateCallback(authenticationData)
} catch (error) {
return { success: false, error: error.message }
}
}
}

View File

@@ -0,0 +1,61 @@
import {
AuthTypes,
Context,
DAL,
FindConfig,
RepositoryService,
} from "@medusajs/types"
import {
InjectManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import { AuthUser } from "@models"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
authUserRepository: DAL.RepositoryService
}
export default class AuthUserService<
TEntity extends AuthUser = AuthUser
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
AuthUser
)<TEntity> {
protected readonly authUserRepository_: RepositoryService<TEntity>
protected baseRepository_: DAL.RepositoryService
constructor(container: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.authUserRepository_ = container.authUserRepository
this.baseRepository_ = container.baseRepository
}
@InjectManager("authUserRepository_")
async retrieveByProviderAndEntityId<TEntityMethod = AuthTypes.AuthUserDTO>(
entityId: string,
provider: string,
config: FindConfig<TEntityMethod> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<AuthTypes.AuthUserDTO> {
const queryConfig = ModulesSdkUtils.buildQuery<TEntity>(
{ entity_id: entityId, provider },
{ ...config, take: 1 }
)
const [result] = await this.authUserRepository_.find(
queryConfig,
sharedContext
)
if (!result) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`AuthUser with entity_id: "${entityId}" and provider: "${provider}" not found`
)
}
return await this.baseRepository_.serialize<AuthTypes.AuthUserDTO>(result)
}
}

View File

@@ -0,0 +1,2 @@
export { default as AuthModuleService } from "./auth-module"
export { default as AuthUserService } from "./auth-user"

View File

@@ -0,0 +1,8 @@
import { Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
}
export * as RepositoryTypes from "./repositories"
export * as ServiceTypes from "./services"

View File

@@ -0,0 +1,19 @@
import { AuthUser } from "@models"
export type CreateAuthUserDTO = {
provider_id: string
entity_id: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown>
}
export type UpdateAuthUserDTO = {
update: {
id: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown>
}
user: AuthUser
}

View File

@@ -0,0 +1 @@
export * from "./auth-user"

View File

@@ -0,0 +1,28 @@
export type AuthUserDTO = {
id: string
provider_id: string
entity_id: string
scope: string
provider: string
provider_metadata?: Record<string, unknown>
user_metadata: Record<string, unknown>
app_metadata: Record<string, unknown>
}
export type CreateAuthUserDTO = {
entity_id: string
provider: string
scope: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown>
}
export type UpdateAuthUserDTO = {
id: string
provider_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
app_metadata?: Record<string, unknown>
}
export type FilterableAuthUserProps = {}

View File

@@ -0,0 +1 @@
export * from "./auth-user"

View File

@@ -0,0 +1,53 @@
{
"compilerOptions": {
"lib": [
"es2020"
],
"target": "es2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": false,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true,
// to use ES5 specific tooling
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@models": [
"./src/models"
],
"@services": [
"./src/services"
],
"@repositories": [
"./src/repositories"
],
"@types": [
"./src/types"
],
"@providers": [
"./src/providers"
]
}
},
"include": [
"src"
],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}

View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,132 @@
# @medusajs/cache-inmemory
## 1.8.10
### Patch Changes
- [#5511](https://github.com/medusajs/medusa/pull/5511) [`80fe362f3`](https://github.com/medusajs/medusa/commit/80fe362f33cba69e52418b57e2e2d476923fc510) Thanks [@adrien2p](https://github.com/adrien2p)! - Integration tests fixes and ignore ttl 0 on cache modules
- Updated dependencies [[`c39bf69a5`](https://github.com/medusajs/medusa/commit/c39bf69a5e5cae75d7fa12aa6022b10903557a32), [`154c9b43b`](https://github.com/medusajs/medusa/commit/154c9b43bde1fdff562aba9da8a79af2660b29b3)]:
- @medusajs/modules-sdk@1.12.3
## 1.8.9
### Patch Changes
- [#5468](https://github.com/medusajs/medusa/pull/5468) [`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa, modules-sdk, modules): Module loading was missing the expected dependencies and remote query reference fix
- Updated dependencies [[`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9)]:
- @medusajs/modules-sdk@1.12.2
## 1.8.8
### Patch Changes
- [#4276](https://github.com/medusajs/medusa/pull/4276) [`afd1b67f1`](https://github.com/medusajs/medusa/commit/afd1b67f1c7de8cf07fd9fcbdde599a37914e9b5) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Use caret range
- Updated dependencies [[`14c0f62f8`](https://github.com/medusajs/medusa/commit/14c0f62f84704a4c87beff3daaff60a52f5c88b8)]:
- @medusajs/modules-sdk@1.8.8
## 1.8.7
### Patch Changes
- Updated dependencies [[`e73c3e51c`](https://github.com/medusajs/medusa/commit/e73c3e51c9cd192eeae7a57b24b07bd466214145)]:
- @medusajs/modules-sdk@1.8.7
## 1.8.6
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.6
## 1.8.5
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.5
## 1.8.4
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.4
## 1.8.3
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.3
## 1.8.2
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.2
## 1.8.1
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.1
## 1.8.0
### Minor Changes
- [#3187](https://github.com/medusajs/medusa/pull/3187) [`f97b3d7cc`](https://github.com/medusajs/medusa/commit/f97b3d7ccee381d3491337ab5144bb44520382a7) Thanks [@fPolic](https://github.com/fPolic)! - feat(medusa, cache-redis, cache-inmemory): Added cache modules
### Patch Changes
- [#3685](https://github.com/medusajs/medusa/pull/3685) [`8ddb3952c`](https://github.com/medusajs/medusa/commit/8ddb3952c045e6c05c8d0f6922f0d4ba30cf3bd4) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Fix RC package versions
- [#3649](https://github.com/medusajs/medusa/pull/3649) [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Export initialize method for all modules
- [#3531](https://github.com/medusajs/medusa/pull/3531) [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Remove dependency on @medusajs/medusa from Inventory and Stock-Location Modules
- Updated dependencies [[`8ddb3952c`](https://github.com/medusajs/medusa/commit/8ddb3952c045e6c05c8d0f6922f0d4ba30cf3bd4), [`55e94d0b4`](https://github.com/medusajs/medusa/commit/55e94d0b45776776639d3970d4264b8f5c5385dd), [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b), [`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73), [`bca1f80dd`](https://github.com/medusajs/medusa/commit/bca1f80dd501d878455e1ad4f5091cf20ef900ea), [`271844aed`](https://github.com/medusajs/medusa/commit/271844aedbe45c369e188b5d06458dbd6984cd39), [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def)]:
- @medusajs/modules-sdk@1.8.0
## 1.8.0-rc.3
### Patch Changes
- [#3649](https://github.com/medusajs/medusa/pull/3649) [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Export initialize method for all modules
- Updated dependencies [[`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b)]:
- @medusajs/modules-sdk@0.1.0-rc.4
## 1.8.0-rc.2
### Patch Changes
- Updated dependencies [[`55e94d0b4`](https://github.com/medusajs/medusa/commit/55e94d0b45776776639d3970d4264b8f5c5385dd)]:
- @medusajs/modules-sdk@0.1.0-rc.3
## 1.8.0-rc.1
### Patch Changes
- chore: Fix RC package versions
- Updated dependencies []:
- @medusajs/modules-sdk@0.1.0-rc.2
## 1.8.0-rc.0
### Minor Changes
- [#3187](https://github.com/medusajs/medusa/pull/3187) [`f97b3d7cc`](https://github.com/medusajs/medusa/commit/f97b3d7ccee381d3491337ab5144bb44520382a7) Thanks [@fPolic](https://github.com/fPolic)! - feat(medusa, cache-redis, cache-inmemory): Added cache modules
### Patch Changes
- [#3531](https://github.com/medusajs/medusa/pull/3531) [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Remove dependency on @medusajs/medusa from Inventory and Stock-Location Modules
- Updated dependencies [[`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73), [`271844aed`](https://github.com/medusajs/medusa/commit/271844aedbe45c369e188b5d06458dbd6984cd39), [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def)]:
- @medusajs/modules-sdk@0.1.0-rc.0

View File

@@ -0,0 +1,23 @@
# Medusa Cache In-memory
Medusa in-memory cache module. Use plain JS Map as a cache store.
## Installation
```
yarn add @medusajs/cache-inmemory
```
## Options
```
{
ttl?: number // Time to keep data in cache (in seconds)
}
```
### Note
Recommended for testing and development. For production, use Redis cache module.
### Other caching modules
- [Medusa Cache Redis](../cache-redis/README.md)

View File

@@ -0,0 +1,13 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
}

View File

@@ -0,0 +1,39 @@
{
"name": "@medusajs/cache-inmemory",
"version": "1.8.10",
"description": "In-memory Cache Module for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/cache-inmemory"
},
"engines": {
"node": ">=16"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/types": "^1.11.6",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"rimraf": "^5.0.1",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
"scripts": {
"watch": "tsc --build --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"build": "rimraf dist && tsc --build",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.3"
}
}

View File

@@ -0,0 +1,12 @@
import { ModuleExports } from "@medusajs/modules-sdk"
import InMemoryCacheService from "./services/inmemory-cache"
const service = InMemoryCacheService
const moduleDefinition: ModuleExports = {
service,
}
export default moduleDefinition
export * from "./initialize"
export * from "./types"

View File

@@ -0,0 +1,23 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
Modules,
} from "@medusajs/modules-sdk"
import { ICacheService } from "@medusajs/types"
import { InMemoryCacheModuleOptions } from "../types"
export const initialize = async (
options?: InMemoryCacheModuleOptions | ExternalModuleDeclaration
): Promise<ICacheService> => {
const serviceKey = Modules.CACHE
const loaded = await MedusaModule.bootstrap<ICacheService>({
moduleKey: serviceKey,
defaultPath: "@medusajs/cache-inmemory",
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
})
return loaded[serviceKey]
}

View File

@@ -0,0 +1,96 @@
import { InMemoryCacheService } from "../index"
jest.setTimeout(40000)
describe("InMemoryCacheService", () => {
let inMemoryCache
beforeEach(() => {
jest.clearAllMocks()
})
it("Stores and retrieves data", async () => {
inMemoryCache = new InMemoryCacheService({}, {})
await inMemoryCache.set("cache-key", { data: "value" })
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
})
it("Invalidates single record", async () => {
inMemoryCache = new InMemoryCacheService({}, {})
await inMemoryCache.set("cache-key", { data: "value" })
await inMemoryCache.invalidate("cache-key")
expect(await inMemoryCache.get("cache-key")).toEqual(null)
})
it("Invalidates multiple keys with wildcard (end matching)", async () => {
inMemoryCache = new InMemoryCacheService({}, {})
await inMemoryCache.set("cache-key:id_1:x:y", { data: "value" })
await inMemoryCache.set("cache-key:id_2:x:y", { data: "value" })
await inMemoryCache.set("cache-key:id_3:x:y", { data: "value" })
await inMemoryCache.set("cache-key-old", { data: "value" })
await inMemoryCache.invalidate("cache-key:*")
expect(await inMemoryCache.get("cache-key:id1:x:y")).toEqual(null)
expect(await inMemoryCache.get("cache-key:id2:x:y")).toEqual(null)
expect(await inMemoryCache.get("cache-key:id3:x:y")).toEqual(null)
expect(await inMemoryCache.get("cache-key-old")).toEqual({ data: "value" })
})
it("Invalidates multiple keys with wildcard (middle matching)", async () => {
inMemoryCache = new InMemoryCacheService({}, {})
await inMemoryCache.set("cache-key:1:new", { data: "value" })
await inMemoryCache.set("cache-key:2:new", { data: "value" })
await inMemoryCache.set("cache-key:3:new", { data: "value" })
await inMemoryCache.set("cache-key:4:old", { data: "value" })
await inMemoryCache.invalidate("cache-key:*:new")
expect(await inMemoryCache.get("cache-key:1:new")).toEqual(null)
expect(await inMemoryCache.get("cache-key:2:new")).toEqual(null)
expect(await inMemoryCache.get("cache-key:3:new")).toEqual(null)
expect(await inMemoryCache.get("cache-key:4:old")).toEqual({
data: "value",
})
})
it("Removes data after TTL", async () => {
inMemoryCache = new InMemoryCacheService({}, {})
await inMemoryCache.set("cache-key", { data: "value" }, 2)
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
await new Promise((res) => setTimeout(res, 3000))
expect(await inMemoryCache.get("cache-key")).toEqual(null)
})
it("Removes data after default TTL if TTL params isn't passed", async () => {
inMemoryCache = new InMemoryCacheService({})
await inMemoryCache.set("cache-key", { data: "value" })
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
await new Promise((res) => setTimeout(res, 33000))
expect(await inMemoryCache.get("cache-key")).toEqual(null)
})
it("Removes data after TTL from the config if TTL params isn't passed", async () => {
inMemoryCache = new InMemoryCacheService({}, { ttl: 1 })
await inMemoryCache.set("cache-key", { data: "value" })
expect(await inMemoryCache.get("cache-key")).toEqual({ data: "value" })
await new Promise((res) => setTimeout(res, 2000))
expect(await inMemoryCache.get("cache-key")).toEqual(null)
})
})

View File

@@ -0,0 +1 @@
export { default as InMemoryCacheService } from "./inmemory-cache"

View File

@@ -0,0 +1,106 @@
import { ICacheService } from "@medusajs/types"
import { CacheRecord, InMemoryCacheModuleOptions } from "../types"
const DEFAULT_TTL = 30 // seconds
type InjectedDependencies = {}
/**
* Class represents basic, in-memory, cache store.
*/
class InMemoryCacheService implements ICacheService {
protected readonly TTL: number
protected readonly store = new Map<string, CacheRecord<any>>()
protected readonly timoutRefs = new Map<string, NodeJS.Timeout>()
constructor(
deps: InjectedDependencies,
options: InMemoryCacheModuleOptions = {}
) {
this.TTL = options.ttl ?? DEFAULT_TTL
}
/**
* Retrieve data from the cache.
* @param key - cache key
*/
async get<T>(key: string): Promise<T | null> {
const now = Date.now()
const record: CacheRecord<T> | undefined = this.store.get(key)
const recordExpire = record?.expire ?? Infinity
if (!record || recordExpire < now) {
return null
}
return record.data
}
/**
* Set data to the cache.
* @param key - cache key under which the data is stored
* @param data - data to be stored in the cache
* @param ttl - expiration time in seconds
*/
async set<T>(key: string, data: T, ttl: number = this.TTL): Promise<void> {
if (ttl === 0) {
return
}
const record: CacheRecord<T> = { data, expire: ttl * 1000 + Date.now() }
const oldRecord = this.store.get(key)
if (oldRecord) {
clearTimeout(this.timoutRefs.get(key))
this.timoutRefs.delete(key)
}
const ref = setTimeout(async () => {
await this.invalidate(key)
}, ttl * 1000)
ref.unref()
this.timoutRefs.set(key, ref)
this.store.set(key, record)
}
/**
* Delete data from the cache.
* Could use wildcard (*) matcher e.g. `invalidate("ps:*")` to delete all keys that start with "ps:"
*
* @param key - cache key
*/
async invalidate(key: string): Promise<void> {
let keys = [key]
if (key.includes("*")) {
const regExp = new RegExp(key.replace("*", ".*"))
keys = Array.from(this.store.keys()).filter((k) => k.match(regExp))
}
keys.forEach((key) => {
const timeoutRef = this.timoutRefs.get(key)
if (timeoutRef) {
clearTimeout(timeoutRef)
this.timoutRefs.delete(key)
}
this.store.delete(key)
})
}
/**
* Delete the entire cache.
*/
async clear() {
this.timoutRefs.forEach((ref) => clearTimeout(ref))
this.timoutRefs.clear()
this.store.clear()
}
}
export default InMemoryCacheService

View File

@@ -0,0 +1,17 @@
/**
* Shape of a record saved in `in-memory` cache
*/
export type CacheRecord<T> = {
data: T
/**
* Timestamp in milliseconds
*/
expire: number
}
export type InMemoryCacheModuleOptions = {
/**
* Time to keep data in cache (in seconds)
*/
ttl?: number
}

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,143 @@
# @medusajs/cache-redis
## 1.9.1
### Patch Changes
- [#6865](https://github.com/medusajs/medusa/pull/6865) [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: medusa shutdown
- Updated dependencies [[`1eeb1e9de3`](https://github.com/medusajs/medusa/commit/1eeb1e9de3e0b571735437b00968ee96e4aabad5), [`e944a627f0`](https://github.com/medusajs/medusa/commit/e944a627f074fb39a56f4bc7b3d6d315736ebf7c), [`dd35a4dbff`](https://github.com/medusajs/medusa/commit/dd35a4dbff10c86ea3c5f7f817c18b6e60d599e3), [`1bcb13f892`](https://github.com/medusajs/medusa/commit/1bcb13f892bc61db21b3fc6bdbce85f747aeec4c), [`82a176e30e`](https://github.com/medusajs/medusa/commit/82a176e30e47a7d11caaf31c3023bd8db588b465), [`528ef4ca90`](https://github.com/medusajs/medusa/commit/528ef4ca90bb2cf6173dccc9fd6a9f9932ff9b76), [`4b57c5d286`](https://github.com/medusajs/medusa/commit/4b57c5d286f9dc6e2098c67e9fecb0d93175b5a1), [`667c8609cc`](https://github.com/medusajs/medusa/commit/667c8609ccf3850f5df8cf784723a95bd0d6d2a6), [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9), [`1c6ba4468e`](https://github.com/medusajs/medusa/commit/1c6ba4468eab1440931c88929affd5b4c593f377)]:
- @medusajs/modules-sdk@1.12.11
## 1.9.0
### Minor Changes
- [#5511](https://github.com/medusajs/medusa/pull/5511) [`80fe362f3`](https://github.com/medusajs/medusa/commit/80fe362f33cba69e52418b57e2e2d476923fc510) Thanks [@adrien2p](https://github.com/adrien2p)! - Integration tests fixes and ignore ttl 0 on cache modules
### Patch Changes
- Updated dependencies [[`c39bf69a5`](https://github.com/medusajs/medusa/commit/c39bf69a5e5cae75d7fa12aa6022b10903557a32), [`154c9b43b`](https://github.com/medusajs/medusa/commit/154c9b43bde1fdff562aba9da8a79af2660b29b3)]:
- @medusajs/modules-sdk@1.12.3
## 1.8.9
### Patch Changes
- [#5468](https://github.com/medusajs/medusa/pull/5468) [`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa, modules-sdk, modules): Module loading was missing the expected dependencies and remote query reference fix
- Updated dependencies [[`a45da9215`](https://github.com/medusajs/medusa/commit/a45da9215d2a7834c368037726aaa3961caadaf9)]:
- @medusajs/modules-sdk@1.12.2
## 1.8.8
### Patch Changes
- [#4276](https://github.com/medusajs/medusa/pull/4276) [`afd1b67f1`](https://github.com/medusajs/medusa/commit/afd1b67f1c7de8cf07fd9fcbdde599a37914e9b5) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Use caret range
- Updated dependencies [[`14c0f62f8`](https://github.com/medusajs/medusa/commit/14c0f62f84704a4c87beff3daaff60a52f5c88b8)]:
- @medusajs/modules-sdk@1.8.8
## 1.8.7
### Patch Changes
- Updated dependencies [[`e73c3e51c`](https://github.com/medusajs/medusa/commit/e73c3e51c9cd192eeae7a57b24b07bd466214145)]:
- @medusajs/modules-sdk@1.8.7
## 1.8.6
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.6
## 1.8.5
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.5
## 1.8.4
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.4
## 1.8.3
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.3
## 1.8.2
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.2
## 1.8.1
### Patch Changes
- Updated dependencies []:
- @medusajs/modules-sdk@1.8.1
## 1.8.0
### Minor Changes
- [#3187](https://github.com/medusajs/medusa/pull/3187) [`f97b3d7cc`](https://github.com/medusajs/medusa/commit/f97b3d7ccee381d3491337ab5144bb44520382a7) Thanks [@fPolic](https://github.com/fPolic)! - feat(medusa, cache-redis, cache-inmemory): Added cache modules
### Patch Changes
- [#3685](https://github.com/medusajs/medusa/pull/3685) [`8ddb3952c`](https://github.com/medusajs/medusa/commit/8ddb3952c045e6c05c8d0f6922f0d4ba30cf3bd4) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Fix RC package versions
- [#3649](https://github.com/medusajs/medusa/pull/3649) [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Export initialize method for all modules
- [#3531](https://github.com/medusajs/medusa/pull/3531) [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Remove dependency on @medusajs/medusa from Inventory and Stock-Location Modules
- Updated dependencies [[`8ddb3952c`](https://github.com/medusajs/medusa/commit/8ddb3952c045e6c05c8d0f6922f0d4ba30cf3bd4), [`55e94d0b4`](https://github.com/medusajs/medusa/commit/55e94d0b45776776639d3970d4264b8f5c5385dd), [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b), [`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73), [`bca1f80dd`](https://github.com/medusajs/medusa/commit/bca1f80dd501d878455e1ad4f5091cf20ef900ea), [`271844aed`](https://github.com/medusajs/medusa/commit/271844aedbe45c369e188b5d06458dbd6984cd39), [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def)]:
- @medusajs/modules-sdk@1.8.0
## 1.8.0-rc.3
### Patch Changes
- [#3649](https://github.com/medusajs/medusa/pull/3649) [`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Export initialize method for all modules
- Updated dependencies [[`bd12a9508`](https://github.com/medusajs/medusa/commit/bd12a95083b69a70b83ad38578c5a68738c41b2b)]:
- @medusajs/modules-sdk@0.1.0-rc.4
## 1.8.0-rc.2
### Patch Changes
- Updated dependencies [[`55e94d0b4`](https://github.com/medusajs/medusa/commit/55e94d0b45776776639d3970d4264b8f5c5385dd)]:
- @medusajs/modules-sdk@0.1.0-rc.3
## 1.8.0-rc.1
### Patch Changes
- chore: Fix RC package versions
- Updated dependencies []:
- @medusajs/modules-sdk@0.1.0-rc.2
## 1.8.0-rc.0
### Minor Changes
- [#3187](https://github.com/medusajs/medusa/pull/3187) [`f97b3d7cc`](https://github.com/medusajs/medusa/commit/f97b3d7ccee381d3491337ab5144bb44520382a7) Thanks [@fPolic](https://github.com/fPolic)! - feat(medusa, cache-redis, cache-inmemory): Added cache modules
### Patch Changes
- [#3531](https://github.com/medusajs/medusa/pull/3531) [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Remove dependency on @medusajs/medusa from Inventory and Stock-Location Modules
- Updated dependencies [[`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73), [`271844aed`](https://github.com/medusajs/medusa/commit/271844aedbe45c369e188b5d06458dbd6984cd39), [`4e9d257d3`](https://github.com/medusajs/medusa/commit/4e9d257d3bf76703ef5be8ca054cc9f0f7339def)]:
- @medusajs/modules-sdk@0.1.0-rc.0

View File

@@ -0,0 +1,26 @@
# Medusa Cache Redis
Use Redis as a Medusa cache store.
## Installation
```
yarn add @medusajs/cache-redis
```
## Options
```
{
ttl?: number // Time to keep data in cache (in seconds)
redisUrl?: string // Redis instance connection string
redisOptions?: RedisOptions // Redis client options
namespace?: string // Prefix for event keys (the default is `medusa:`)
}
```
### Other caching modules
- [Medusa Cache In-Memory](../cache-inmemory/README.md)

View File

@@ -0,0 +1,13 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
}

View File

@@ -0,0 +1,41 @@
{
"name": "@medusajs/cache-redis",
"version": "1.9.1",
"description": "Redis Cache Module for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/cache-redis"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=16"
},
"files": [
"dist"
],
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/types": "^1.11.16",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"rimraf": "^5.0.1",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
"scripts": {
"watch": "tsc --build --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"build": "rimraf dist && tsc --build",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.11",
"awilix": "^8.0.0",
"ioredis": "^5.3.1"
}
}

View File

@@ -0,0 +1,15 @@
import { ModuleExports } from "@medusajs/modules-sdk"
import Loader from "./loaders"
import { RedisCacheService } from "./services"
const service = RedisCacheService
const loaders = [Loader]
const moduleDefinition: ModuleExports = {
service,
loaders,
}
export default moduleDefinition
export * from "./initialize"
export * from "./types"

View File

@@ -0,0 +1,23 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
Modules,
} from "@medusajs/modules-sdk"
import { ICacheService } from "@medusajs/types"
import { RedisCacheModuleOptions } from "../types"
export const initialize = async (
options?: RedisCacheModuleOptions | ExternalModuleDeclaration
): Promise<ICacheService> => {
const serviceKey = Modules.CACHE
const loaded = await MedusaModule.bootstrap<ICacheService>({
moduleKey: serviceKey,
defaultPath: "@medusajs/cache-redis",
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
})
return loaded[serviceKey]
}

View File

@@ -0,0 +1,37 @@
import { LoaderOptions } from "@medusajs/modules-sdk"
import { asValue } from "awilix"
import Redis from "ioredis"
import { RedisCacheModuleOptions } from "../types"
export default async ({
container,
logger,
options,
}: LoaderOptions): Promise<void> => {
const { redisUrl, redisOptions } = options as RedisCacheModuleOptions
if (!redisUrl) {
throw Error(
"No `redisUrl` provided in `cacheService` module options. It is required for the Redis Cache Module."
)
}
const connection = new Redis(redisUrl, {
// Lazy connect to properly handle connection errors
lazyConnect: true,
...(redisOptions ?? {}),
})
try {
await connection.connect()
logger?.info(`Connection to Redis in module 'cache-redis' established`)
} catch (err) {
logger?.error(
`An error occurred while connecting to Redis in module 'cache-redis': ${err}`
)
}
container.register({
cacheRedisConnection: asValue(connection),
})
}

View File

@@ -0,0 +1,29 @@
import { RedisCacheService } from "../index"
const redisClientMock = {
set: jest.fn(),
get: jest.fn(),
}
describe("RedisCacheService", () => {
let cacheService
beforeEach(() => {
jest.clearAllMocks()
})
it("Underlying client methods are called", async () => {
cacheService = new RedisCacheService(
{
cacheRedisConnection: redisClientMock,
},
{}
)
await cacheService.set("test-key", "value")
expect(redisClientMock.set).toBeCalled()
await cacheService.get("test-key")
expect(redisClientMock.get).toBeCalled()
})
})

View File

@@ -0,0 +1 @@
export { default as RedisCacheService } from "./redis-cache"

View File

@@ -0,0 +1,98 @@
import { ICacheService } from "@medusajs/types"
import { Redis } from "ioredis"
import { RedisCacheModuleOptions } from "../types"
const DEFAULT_NAMESPACE = "medusa"
const DEFAULT_CACHE_TIME = 30 // 30 seconds
const EXPIRY_MODE = "EX" // "EX" stands for an expiry time in second
type InjectedDependencies = {
cacheRedisConnection: Redis
}
class RedisCacheService implements ICacheService {
protected readonly TTL: number
protected readonly redis: Redis
private readonly namespace: string
constructor(
{ cacheRedisConnection }: InjectedDependencies,
options: RedisCacheModuleOptions = {}
) {
this.redis = cacheRedisConnection
this.TTL = options.ttl ?? DEFAULT_CACHE_TIME
this.namespace = options.namespace || DEFAULT_NAMESPACE
}
__hooks = {
onApplicationShutdown: async () => {
this.redis.disconnect()
},
}
/**
* Set a key/value pair to the cache.
* If the ttl is 0 it will act like the value should not be cached at all.
* @param key
* @param data
* @param ttl
*/
async set(
key: string,
data: Record<string, unknown>,
ttl: number = this.TTL
): Promise<void> {
if (ttl === 0) {
return
}
await this.redis.set(
this.getCacheKey(key),
JSON.stringify(data),
EXPIRY_MODE,
ttl
)
}
/**
* Retrieve a cached value belonging to the given key.
* @param cacheKey
*/
async get<T>(cacheKey: string): Promise<T | null> {
cacheKey = this.getCacheKey(cacheKey)
try {
const cached = await this.redis.get(cacheKey)
if (cached) {
return JSON.parse(cached)
}
} catch (err) {
await this.redis.del(cacheKey)
}
return null
}
/**
* Invalidate cache for a specific key. a key can be either a specific key or more global such as "ps:*".
* @param key
*/
async invalidate(key: string): Promise<void> {
const keys = await this.redis.keys(this.getCacheKey(key))
const pipeline = this.redis.pipeline()
keys.forEach(function (key) {
pipeline.del(key)
})
await pipeline.exec()
}
/**
* Returns namespaced cache key
* @param key
*/
private getCacheKey(key: string) {
return this.namespace ? `${this.namespace}:${key}` : key
}
}
export default RedisCacheService

View File

@@ -0,0 +1,27 @@
import { RedisOptions } from "ioredis"
/**
* Module config type
*/
export type RedisCacheModuleOptions = {
/**
* Time to keep data in cache (in seconds)
*/
ttl?: number
/**
* Redis connection string
*/
redisUrl?: string
/**
* Redis client options
*/
redisOptions?: RedisOptions
/**
* Prefix for event keys
* @default `medusa:`
*/
namespace?: string
}

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

6
packages/modules/cart/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

@@ -0,0 +1,21 @@
# @medusajs/cart
## 0.0.3
### Patch Changes
- [`cc557c8752`](https://github.com/medusajs/medusa/commit/cc557c8752fd0554f5a1b58522d9a88dc43a8509) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - Order endpoints and Cart totals
- Updated dependencies [[`1eeb1e9de3`](https://github.com/medusajs/medusa/commit/1eeb1e9de3e0b571735437b00968ee96e4aabad5), [`20e8df914e`](https://github.com/medusajs/medusa/commit/20e8df914ec5fdf8d562d4fa84f72c58c7056195), [`e0b02a1012`](https://github.com/medusajs/medusa/commit/e0b02a1012981c29830d7779f59ebe805bbfd137), [`e944a627f0`](https://github.com/medusajs/medusa/commit/e944a627f074fb39a56f4bc7b3d6d315736ebf7c), [`cc557c8752`](https://github.com/medusajs/medusa/commit/cc557c8752fd0554f5a1b58522d9a88dc43a8509), [`dd35a4dbff`](https://github.com/medusajs/medusa/commit/dd35a4dbff10c86ea3c5f7f817c18b6e60d599e3), [`1bcb13f892`](https://github.com/medusajs/medusa/commit/1bcb13f892bc61db21b3fc6bdbce85f747aeec4c), [`82a176e30e`](https://github.com/medusajs/medusa/commit/82a176e30e47a7d11caaf31c3023bd8db588b465), [`5d9aea053c`](https://github.com/medusajs/medusa/commit/5d9aea053ce6e04f242f86fb9053c13dec515d5b), [`232322d035`](https://github.com/medusajs/medusa/commit/232322d03515f81e56867ff8c765b8409399ee68), [`528ef4ca90`](https://github.com/medusajs/medusa/commit/528ef4ca90bb2cf6173dccc9fd6a9f9932ff9b76), [`4b57c5d286`](https://github.com/medusajs/medusa/commit/4b57c5d286f9dc6e2098c67e9fecb0d93175b5a1), [`667c8609cc`](https://github.com/medusajs/medusa/commit/667c8609ccf3850f5df8cf784723a95bd0d6d2a6), [`a6562d2a41`](https://github.com/medusajs/medusa/commit/a6562d2a41453cbe7aa43be352c4924e3e4c79d5), [`8fd1488938`](https://github.com/medusajs/medusa/commit/8fd148893850eb66c5eae00c4ca9391a80ea2eb9), [`1c6ba4468e`](https://github.com/medusajs/medusa/commit/1c6ba4468eab1440931c88929affd5b4c593f377)]:
- @medusajs/modules-sdk@1.12.11
- @medusajs/utils@1.11.9
## 0.0.2
### Patch Changes
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`586df9da25`](https://github.com/medusajs/medusa/commit/586df9da250e492442769f5bac2f8b3de1d46f05), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`d550be3685`](https://github.com/medusajs/medusa/commit/d550be3685423218d47a20c57a5e06758f4a961a), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`4625bd1241`](https://github.com/medusajs/medusa/commit/4625bd12416275b09c22cde4a09cb0f68df5d7c1), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`24fb102a56`](https://github.com/medusajs/medusa/commit/24fb102a564b1253d1f8b039bb1e435cc5312fbb), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/utils@1.11.7
- @medusajs/modules-sdk@1.12.9

View File

@@ -0,0 +1,3 @@
# Cart Module
A Cart is a collection of items that a customer intends to purchase. The Cart also stores where the items should be shipped, how they should be shipped, how the goods will be paid for and who the customer is. The Cart facilitates calculations of totals and validation of purchase flows.

View File

@@ -0,0 +1,23 @@
export const defaultCartsData = [
{
id: "cart-id-1",
region_id: "region-id-1",
customer_id: "customer-id-1",
email: "test@email.com",
currency_code: "usd",
},
{
id: "cart-id-2",
region_id: "region-id-1",
customer_id: "customer-id-1",
shipping_address: {
first_name: "Tony",
last_name: "Stark",
},
billing_address: {
address_1: "Stark Industries",
city: "New York",
},
currency_code: "usd",
},
]

View File

@@ -0,0 +1,21 @@
import { CreateCartDTO } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Cart } from "../../../src/models"
import { defaultCartsData } from "./data"
export * from "./data"
export async function createCarts(
manager: SqlEntityManager,
cartsData: CreateCartDTO[] = defaultCartsData
): Promise<Cart[]> {
const carts: Cart[] = []
for (let cartData of cartsData) {
let cart = manager.create(Cart, cartData)
await manager.persistAndFlush(cart)
}
return carts
}

View File

@@ -0,0 +1,21 @@
module.exports = {
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@types": "<rootDir>/src/types",
"^@utils": "<rootDir>/src/utils",
},
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.spec.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
}

View File

@@ -0,0 +1,8 @@
import * as entities from "./src/models"
module.exports = {
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-cart",
type: "postgresql",
}

View File

@@ -0,0 +1,61 @@
{
"name": "@medusajs/cart",
"version": "0.0.3",
"description": "Medusa Cart module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"bin": {
"medusa-cart-seed": "dist/scripts/bin/run-seed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/cart"
},
"publishConfig": {
"access": "public"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
"test": "jest --runInBand --bail --passWithNoTests --forceExit -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/types": "workspace:^",
"@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"medusa-test-utils": "^1.1.44",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.6",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.11",
"@medusajs/utils": "^1.11.9",
"@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7",
"@mikro-orm/postgresql": "5.9.7",
"awilix": "^8.0.0",
"dotenv": "^16.4.5",
"knex": "2.4.2"
}
}

View File

@@ -0,0 +1,10 @@
import {
moduleDefinition,
revertMigration,
runMigrations,
} from "./module-definition"
export default moduleDefinition
export { revertMigration, runMigrations }
export * from "./initialize"

View File

@@ -0,0 +1,31 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
Modules,
} from "@medusajs/modules-sdk"
import { ICartModuleService, ModulesSdkTypes } from "@medusajs/types"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "@types"
export const initialize = async (
options?:
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
| ExternalModuleDeclaration
| InternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<ICartModuleService> => {
const loaded = await MedusaModule.bootstrap<ICartModuleService>({
moduleKey: Modules.CART,
defaultPath: MODULE_PACKAGE_NAMES[Modules.CART],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.CART]
}

Some files were not shown because too many files have changed in this diff Show More