Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default defineConfig(
'bin/**',
'dist/**',
'build/**',
'patches/**',
'node_modules/**',
])
],
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"test:ci": "vitest run",
"test:integration": "vitest run --reporter=verbose tests/integration/",
"test:integration:watch": "vitest tests/integration/",
"db:migrate": "prisma migrate dev",
"prisma": "tsx node_modules/prisma/build/index.js",
"prisma:generate": "tsx node_modules/prisma/build/index.js generate",
"db:migrate": "tsx node_modules/prisma/build/index.js migrate dev",
"seed": "tsx prisma/seed.ts",
"seed:reset": "tsx prisma/seed.ts --reset",
"db:seed": "npm run seed",
"db:studio": "prisma studio"
"db:studio": "tsx node_modules/prisma/build/index.js studio"
},
"dependencies": {
"@prisma/adapter-pg": "^7.4.2",
Expand Down Expand Up @@ -90,6 +92,11 @@
"doc": "docs",
"test": "tests"
},
"pnpm": {
"overrides": {
"zeptomatch": "file:patches/zeptomatch-cjs-shim"
}
},
"bugs": {
"url": "https://github.com/learnault/learnault/issues"
}
Expand Down
85 changes: 85 additions & 0 deletions patches/zeptomatch-cjs-shim/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

/**
* Minimal CJS zeptomatch-compatible glob matcher.
* Implements the subset of zeptomatch's API used by @prisma/dev:
* zeptomatch(pattern, path) → boolean
*/

const SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;

function escapeRegex(str) {
return str.replace(SPECIAL_CHARS, '\\$&');
}

function compilePattern(pattern) {
let regexStr = '^';
let i = 0;
const len = pattern.length;

while (i < len) {
const ch = pattern[i];

if (ch === '*') {
if (pattern[i + 1] === '*') {
regexStr += '.*';
i += 2;
if (pattern[i] === '/') i++;
} else {
regexStr += '[^/]*';
i++;
}
} else if (ch === '?') {
regexStr += '[^/]';
i++;
} else if (ch === '{') {
let j = i + 1;
let depth = 1;
while (j < len && depth > 0) {
if (pattern[j] === '{') depth++;
else if (pattern[j] === '}') depth--;
j++;
}
const alternatives = pattern.slice(i + 1, j - 1).split(',');
regexStr += '(' + alternatives.map(escapeRegex).join('|') + ')';
i = j;
} else if (ch === '[') {
let j = i + 1;
while (j < len && pattern[j] !== ']') j++;
regexStr += pattern.slice(i, j + 1);
i = j + 1;
} else if (ch === '\\') {
regexStr += escapeRegex(pattern[i + 1]);
i += 2;
} else {
regexStr += escapeRegex(ch);
i++;
}
}

regexStr += '$';
return new RegExp(regexStr);
}

const cache = new Map();

function zeptomatch(pattern, path) {
if (typeof pattern !== 'string') return false;
let re = cache.get(pattern);
if (!re) {
re = compilePattern(pattern);
cache.set(pattern, re);
}
return re.test(path);
}

zeptomatch.compile = function compileGlob(pattern) {
if (typeof pattern === 'string') {
const re = compilePattern(pattern);
return { test: (path) => re.test(path) };
}
return { test: () => false };
};

module.exports = zeptomatch;
module.exports.default = zeptomatch;
10 changes: 10 additions & 0 deletions patches/zeptomatch-cjs-shim/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "zeptomatch-cjs-shim",
"version": "2.1.0",
"description": "CJS shim for zeptomatch, used by @prisma/dev on Node 20",
"main": "index.js",
"type": "commonjs",
"exports": {
".": "./index.js"
}
}
30 changes: 30 additions & 0 deletions patches/zeptomatch-cjs-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { createRequire } = require('node:module');
const { pathToFileURL } = require('node:url');

// Resolve the ESM entry point from the real zeptomatch package
const esmPath = require.resolve('zeptomatch/dist/index.js', { paths: __dirname });

let cached = null;

async function loadZeptomatch() {
if (cached) return cached;
const mod = await import(pathToFileURL(esmPath).href);
cached = mod.default;
return cached;
}

// Synchronous wrapper that returns a thenable matching zeptomatch's API.
// Prisma only uses zeptomatch synchronously (compile + test), so we can
// eagerly load the module at require-time using import().
const zeptomatchSync = (glob, path, options) => {
throw new Error('zeptomatch-cjs: async-only mode; use the ESM entry point');
};

module.exports = zeptomatchSync;
module.exports.default = zeptomatchSync;

// Pre-load in background so it's ready for synchronous use
loadZeptomatch().then(fn => {
module.exports = fn;
module.exports.default = fn;
}).catch(() => {});
86 changes: 86 additions & 0 deletions patches/zeptomatch-cjs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Minimal CJS zeptomatch-compatible glob matcher.
* Implements the subset of zeptomatch's API used by @prisma/dev:
* zeptomatch(pattern, path) → boolean
*/
'use strict';

const SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;

function escapeRegex(str) {
return str.replace(SPECIAL_CHARS, '\\$&');
}

function compile(pattern) {
let regexStr = '^';
let i = 0;
const len = pattern.length;

while (i < len) {
const ch = pattern[i];

if (ch === '*') {
if (pattern[i + 1] === '*') {
// ** — match anything including /
regexStr += '.*';
i += 2;
if (pattern[i] === '/') i++; // skip trailing slash after **/
} else {
// * — match anything except /
regexStr += '[^/]*';
i++;
}
} else if (ch === '?') {
regexStr += '[^/]';
i++;
} else if (ch === '{') {
// Find closing brace
let j = i + 1;
let depth = 1;
while (j < len && depth > 0) {
if (pattern[j] === '{') depth++;
else if (pattern[j] === '}') depth--;
j++;
}
const alternatives = pattern.slice(i + 1, j - 1).split(',');
regexStr += '(' + alternatives.map(escapeRegex).join('|') + ')';
i = j;
} else if (ch === '[') {
let j = i + 1;
while (j < len && pattern[j] !== ']') j++;
regexStr += pattern.slice(i, j + 1);
i = j + 1;
} else if (ch === '\\') {
regexStr += escapeRegex(pattern[i + 1]);
i += 2;
} else {
regexStr += escapeRegex(ch);
i++;
}
}

regexStr += '$';
return new RegExp(regexStr);
}

const cache = new Map();

function zeptomatch(pattern, path, options) {
if (typeof pattern !== 'string') return false;
let re = cache.get(pattern);
if (!re) {
re = compile(pattern);
cache.set(pattern, re);
}
return re.test(path);
}

zeptomatch.compile = function compileGlob(pattern, options) {
if (typeof pattern === 'string') {
return { test: (path) => zeptomatch(pattern, path, options) };
}
return { test: () => false };
};

module.exports = zeptomatch;
module.exports.default = zeptomatch;
24 changes: 7 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions prisma.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
import { defineConfig } from 'prisma/config'

export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
url: process.env.DATABASE_URL ?? 'postgresql://user:password@localhost:5432/learnault',
},
})
44 changes: 44 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ model User {
onboarding OnboardingProgress?
consentRecords ConsentRecord[]
wallet Wallet?
avatars Avatar[]

@@map("users")
}
Expand Down Expand Up @@ -554,3 +555,46 @@ model WalletProvisioningJob {
@@index([status, leasedUntil])
@@map("wallet_provisioning_jobs")
}

model Avatar {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
storageKey String
originalName String?
contentType String // declared MIME from upload intent
detectedMime String? // MIME after server-side sniffing (null before finalize)
originalBytes Int @default(0)
status String @default("PENDING") // PENDING, PROCESSING, ACTIVE, FAILED
scanResult String? // clean, rejected, error
scanReason String?
width Int?
height Int?
variantCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
finalizedAt DateTime?
replacedAt DateTime?
replacedById String?
replacedBy Avatar? @relation("AvatarReplacement", fields: [replacedById], references: [id])
replacements Avatar[] @relation("AvatarReplacement")
variants AvatarVariant[]

@@index([userId, status])
@@map("avatars")
}

model AvatarVariant {
id String @id @default(uuid())
avatarId String
avatar Avatar @relation(fields: [avatarId], references: [id], onDelete: Cascade)
label String // original, thumb, medium
storageKey String
bytes Int @default(0)
width Int?
height Int?
createdAt DateTime @default(now())

@@index([avatarId])
@@map("avatar_variants")
}
Loading
Loading