Skip to content

Commit

Permalink
Adds new random identifier type (#8726)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <[email protected]>
  • Loading branch information
dcousens and dcousens authored Aug 1, 2023
1 parent 7d92235 commit 404d0d7
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 166 deletions.
5 changes: 0 additions & 5 deletions .changeset/add-cuid2.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/add-random-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': minor
---

Adds new random identifier type as `db: { idField: { kind: 'random', bytes?: number, encoding?: 'hex' | 'base64url' } }`, with a default of 32 bytes, encoded as `base64url`
2 changes: 1 addition & 1 deletion .changeset/fix-field-widths.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'@keystone-6/admin-ui': patch
'@keystone-6/core': patch
---

Fix field width of grouped fields
2 changes: 1 addition & 1 deletion .changeset/fix-responsive-ui.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'@keystone-6/admin-ui': minor
'@keystone-6/core': minor
---

Adds responsive AdminUI menu for smaller device widths
3 changes: 3 additions & 0 deletions examples/custom-id/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export default config({
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',

// our default identifier type can be 128-bit hex strings
idField: { kind: 'random', bytes: 16, encoding: 'hex' },

// WARNING: this is only needed for our monorepo examples, don't do this
...fixPrismaPath,
},
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-id/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const lists: Lists = {
Person: list({
access: allowAll,
db: {
idField: { kind: 'cuid2' },
idField: { kind: 'random' },
},
fields: {
name: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
Expand Down
30 changes: 17 additions & 13 deletions examples/default-values/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export const lists: Lists = {
],
hooks: {
resolveInput({ resolvedData, inputData }) {
if (inputData.priority === undefined) {
// default to high if "urgent" is in the label
if (inputData.priority === null) {
// default to high if "urgent" is in the label
if (inputData.label && inputData.label.toLowerCase().includes('urgent')) {
return 'high';
} else {
Expand All @@ -30,33 +30,36 @@ export const lists: Lists = {
},
},
}),
// Static default: When a task is first created, it is incomplete

// static default: when a task is first created, it is incomplete
isComplete: checkbox({ defaultValue: false }),

assignedTo: relationship({
ref: 'Person.tasks',
many: false,
hooks: {
// Dynamic default: Find an anonymous user and assign the task to them
// dynamic default: if unassigned, find an anonymous user and assign the task to them
async resolveInput({ context, operation, resolvedData }) {
if (operation === 'create' && !resolvedData.assignedTo) {
const anonymous = await context.db.Person.findMany({
if (resolvedData.assignedTo === null) {
const [user] = await context.db.Person.findMany({
where: { name: { equals: 'Anonymous' } },
});
if (anonymous.length > 0) {
return { connect: { id: anonymous[0].id } };

if (user) {
return { connect: { id: user.id } };
}
}
// If we don't have an anonymous user we return the value
// that was passed in(which might be nothing) so as not to apply any default

return resolvedData.assignedTo;
},
},
}),
// Dynamic default: We set the due date to be 7 days in the future

// dynamic default: we set the due date to be 7 days in the future
finishBy: timestamp({
hooks: {
resolveInput({ resolvedData, inputData, operation }) {
if (inputData.finishBy == null && operation === 'create') {
if (inputData.finishBy == null) {
const date = new Date();
date.setUTCDate(new Date().getUTCDate() + 7);
return date;
Expand All @@ -65,7 +68,8 @@ export const lists: Lists = {
},
},
}),
// Static default: When a task is first created, it has been viewed zero times

// static default: when a task is first created, it has been viewed zero times
viewCount: bigInt({
defaultValue: 0n,
}),
Expand Down
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@
"@keystone-ui/toast": "^6.0.2",
"@keystone-ui/tooltip": "^6.0.2",
"@nodelib/fs.walk": "^2.0.0",
"@paralleldrive/cuid2": "^2.2.1",
"@prisma/client": "4.16.2",
"@prisma/internals": "4.16.2",
"@prisma/migrate": "4.16.2",
Expand Down
42 changes: 3 additions & 39 deletions packages/core/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
import type { KeystoneConfig, IdFieldConfig } from '../types';
import type { KeystoneConfig } from '../types';
import { idFieldType } from './id-field';

// TODO: move to system/initialisation
function getIdField({ kind, type }: IdFieldConfig): Required<IdFieldConfig> {
if (kind === 'cuid') return { kind: 'cuid', type: 'String' };
if (kind === 'cuid2') return { kind: 'cuid2', type: 'String' };
if (kind === 'uuid') return { kind: 'uuid', type: 'String' };
if (kind === 'string') return { kind: 'string', type: 'String' };
if (kind === 'autoincrement') {
if (type === 'BigInt') return { kind: 'autoincrement', type: 'BigInt' };
return { kind: 'autoincrement', type: 'Int' };
}

throw new Error(`Unknown id type ${kind}`);
}

// validate lists config and default the id field
function applyIdFieldDefaults(config: KeystoneConfig): KeystoneConfig['lists'] {
const defaultIdField = getIdField(config.db.idField ?? { kind: 'cuid' });
if (
defaultIdField.kind === 'autoincrement' &&
defaultIdField.type === 'BigInt' &&
config.db.provider === 'sqlite'
) {
throw new Error(
'BigInt autoincrements are not supported on SQLite but they are configured as the global id field type at db.idField'
);
}

// some error checking
for (const [listKey, list] of Object.entries(config.lists)) {
if (list.fields.id) {
Expand All @@ -38,24 +12,14 @@ function applyIdFieldDefaults(config: KeystoneConfig): KeystoneConfig['lists'] {
);
}

if (
list.db?.idField?.kind === 'autoincrement' &&
list.db.idField.type === 'BigInt' &&
config.db.provider === 'sqlite'
) {
throw new Error(
`BigInt autoincrements are not supported on SQLite but they are configured at db.idField on the ${listKey} list`
);
}

if (list.isSingleton && list.db?.idField) {
throw new Error(
`A singleton list cannot specify an idField, but it is configured at db.idField on the ${listKey} list`
);
}
}

// inject the ID fields
// inject ID fields
const listsWithIds: KeystoneConfig['lists'] = {};

for (const [listKey, list] of Object.entries(config.lists)) {
Expand All @@ -81,7 +45,7 @@ function applyIdFieldDefaults(config: KeystoneConfig): KeystoneConfig['lists'] {
listsWithIds[listKey] = {
...list,
fields: {
id: idFieldType(getIdField(list.db?.idField ?? defaultIdField), false),
id: idFieldType(list.db?.idField ?? config.db.idField ?? { kind: 'cuid' }, false),
...list.fields,
},
};
Expand Down
42 changes: 37 additions & 5 deletions packages/core/src/lib/createSystem.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes } from 'node:crypto';
import pLimit from 'p-limit';
import type { FieldData, KeystoneConfig } from '../types';

Expand All @@ -6,7 +7,7 @@ import type { PrismaModule } from '../artifacts';
import { allowAll } from '../access';
import { createGraphQLSchema } from './createGraphQLSchema';
import { createContext } from './context/createContext';
import { initialiseLists } from './core/initialise-lists';
import { initialiseLists, InitialisedList } from './core/initialise-lists';
import { setPrismaNamespace, setWriteLimit } from './core/utils';

function getSudoGraphQLSchema(config: KeystoneConfig) {
Expand Down Expand Up @@ -66,6 +67,36 @@ function getSudoGraphQLSchema(config: KeystoneConfig) {
// return createGraphQLSchema(transformedConfig, lists, null, true);
}

function injectNewDefaults(prismaClient: any, lists: Record<string, InitialisedList>) {
for (const listKey in lists) {
const list = lists[listKey];

// TODO: other fields might use 'random' too
const { dbField } = list.fields.id;

if ('default' in dbField && dbField.default?.kind === 'random') {
const { bytes, encoding } = dbField.default;
prismaClient = prismaClient.$extends({
query: {
[list.prisma.listKey]: {
async create({ model, args, query }: any) {
return query({
...args,
data: {
...args.data,
id: args.data.id ?? randomBytes(bytes).toString(encoding),
},
});
},
},
},
});
}
}

return prismaClient;
}

export function createSystem(config: KeystoneConfig) {
const lists = initialiseLists(config);
const adminMeta = createAdminMeta(config, lists);
Expand All @@ -76,16 +107,17 @@ export function createSystem(config: KeystoneConfig) {
graphQLSchema,
adminMeta,
getKeystone: (prismaModule: PrismaModule) => {
const prismaClient = new prismaModule.PrismaClient({
const prePrismaClient = new prismaModule.PrismaClient({
datasources: { [config.db.provider]: { url: config.db.url } },
log:
config.db.enableLogging === true
? ['query']
: config.db.enableLogging === false
? undefined
: config.db.enableLogging,
datasources: { [config.db.provider]: { url: config.db.url } },
});

const prismaClient = injectNewDefaults(prePrismaClient, lists);
setWriteLimit(prismaClient, pLimit(config.db.provider === 'sqlite' ? 1 : Infinity));
setPrismaNamespace(prismaClient, prismaModule.Prisma);

Expand All @@ -98,12 +130,12 @@ export function createSystem(config: KeystoneConfig) {
});

return {
// TODO: remove, replace with server.onStart, remove in breaking change
// TODO: replace with server.onStart, remove in breaking change
async connect() {
await prismaClient.$connect();
await config.db.onConnect?.(context);
},
// TODO: remove, only used by tests, remove in breaking change
// TODO: only used by tests, remove in breaking change
async disconnect() {
await prismaClient.$disconnect();
},
Expand Down
Loading

0 comments on commit 404d0d7

Please sign in to comment.