@payloadcms/figma 0.0.1-alpha.56 → 0.0.1-alpha.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -184,7 +184,7 @@ import { loginCommand } from './login.js';
184
184
  buildInfo = await detectBuild(projectPath);
185
185
  if (!buildInfo) {
186
186
  p.log.error(pc.red('✗ Build completed but standalone build not found'));
187
- p.note('Ensure next.config.js has output: "standalone"', 'Check Lambda Configuration');
187
+ p.note('Ensure next.config has output: "standalone"', 'Check Lambda Configuration');
188
188
  process.exit(1);
189
189
  }
190
190
  }
@@ -476,6 +476,7 @@ async function create(args) {
476
476
  locale,
477
477
  ...buildMeta(this.payload, {
478
478
  collection: args.collection,
479
+ data: args.data,
479
480
  locale
480
481
  })
481
482
  }
@@ -584,6 +585,7 @@ async function upsert(args) {
584
585
  where: convertPayloadWhereToContentAPI(args.where),
585
586
  ...buildMeta(this.payload, {
586
587
  collection: args.collection,
588
+ data: args.data,
587
589
  locale,
588
590
  where: args.where
589
591
  })
@@ -157,6 +157,15 @@ export function dataFromContentAPI(payload, collectionSlug, data) {
157
157
  return;
158
158
  }
159
159
  if (value !== null) {
160
+ // hasMany relationships: normalize to array (non-localized only).
161
+ // Unlike Drizzle (which reconstructs arrays from join tables), we store raw JSON,
162
+ // so a single-item input like `{ value, relationTo }` stays as-is. Payload expects arrays.
163
+ // Localized fields are skipped because their value is a locale map, not a relationship value.
164
+ if (field.type === 'relationship' && field.hasMany && !Array.isArray(value) && !('localized' in field && field.localized)) {
165
+ current[field.name] = [
166
+ value
167
+ ];
168
+ }
160
169
  // Localized fields: JSON string -> parsed object
161
170
  // Content API may store localized fields as JSON strings like "{\"en\":\"value\"}"
162
171
  // so we need to parse them back to objects.
@@ -11,6 +11,9 @@ import type { PathTypesRecord } from '../../temp-utilities/types.js';
11
11
  export interface ContentAPIMeta {
12
12
  localizedPaths?: string[];
13
13
  pathTypes?: PathTypesRecord;
14
+ uniquePaths?: {
15
+ paths: string[];
16
+ }[];
14
17
  }
15
18
  /**
16
19
  * The type that Content API currently expects (from generated types).
@@ -19,6 +22,8 @@ export interface ContentAPIMeta {
19
22
  type ContentAPIMetaGenerated = components['schemas']['RequestMeta'];
20
23
  export interface BuildMetaOptions {
21
24
  collection: string;
25
+ /** Document data — when provided, unique field constraints are included in meta. */
26
+ data?: Record<string, unknown>;
22
27
  locale: string | undefined;
23
28
  where?: Where;
24
29
  }
@@ -1,5 +1,6 @@
1
1
  import { buildLocalizedPaths } from './buildLocalizedPaths.js';
2
2
  import { buildPathTypes } from './buildPathTypes.js';
3
+ import { buildUniquePaths } from './buildUniquePaths.js';
3
4
  /**
4
5
  * Builds the `meta` object for Content API requests.
5
6
  * Combines pathTypes (for array field handling) and localizedPaths (for locale queries).
@@ -8,7 +9,7 @@ import { buildPathTypes } from './buildPathTypes.js';
8
9
  * @param options - Options including collection slug, locale, and where clause
9
10
  * @returns Object with meta property ready to spread into request body, or empty object if no meta needed
10
11
  */ export function buildMeta(payload, options) {
11
- const { collection, locale, where } = options;
12
+ const { collection, data, locale, where } = options;
12
13
  const meta = {};
13
14
  // Add pathTypes if there are array fields in the where clause
14
15
  const pathTypes = buildPathTypes(payload, collection, where);
@@ -22,6 +23,13 @@ import { buildPathTypes } from './buildPathTypes.js';
22
23
  meta.localizedPaths = localizedPaths;
23
24
  }
24
25
  }
26
+ // Add uniquePaths when data is provided (create/upsert operations)
27
+ if (data) {
28
+ const uniquePaths = buildUniquePaths(payload, collection, data);
29
+ if (uniquePaths.length > 0) {
30
+ meta.uniquePaths = uniquePaths;
31
+ }
32
+ }
25
33
  // Cast to generated type - Content API will need to be updated to handle
26
34
  // the new pathTypes format with relationship info. Until then, it will
27
35
  // ignore the extra fields but still receive the data for testing.
@@ -0,0 +1,13 @@
1
+ import type { Payload } from 'payload';
2
+ import type { components } from '../../generated/content-api-types.js';
3
+ type UniquePath = components['schemas']['UniquePath'];
4
+ /**
5
+ * Builds a UniquePath[] from the collection's field config by collecting
6
+ * all fields with `unique: true` that have a non-null value in the data.
7
+ *
8
+ * Fields with null/undefined values are excluded to match sparse unique index
9
+ * behavior (multiple documents can have null for a unique field).
10
+ */
11
+ export declare function buildUniquePaths(payload: Payload, collectionSlug: string, data: Record<string, unknown>): UniquePath[];
12
+ export {};
13
+ //# sourceMappingURL=buildUniquePaths.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Builds a UniquePath[] from the collection's field config by collecting
3
+ * all fields with `unique: true` that have a non-null value in the data.
4
+ *
5
+ * Fields with null/undefined values are excluded to match sparse unique index
6
+ * behavior (multiple documents can have null for a unique field).
7
+ */ export function buildUniquePaths(payload, collectionSlug, data) {
8
+ const isGlobal = collectionSlug.startsWith('_global-');
9
+ const actualSlug = isGlobal ? collectionSlug.substring(8) : collectionSlug;
10
+ const config = isGlobal ? payload.config.globals?.find((g)=>g.slug === actualSlug) : payload.config.collections.find((c)=>c.slug === actualSlug);
11
+ if (!config?.fields) {
12
+ return [];
13
+ }
14
+ const unique = [];
15
+ collectUniqueFields(config.fields, '', data, unique);
16
+ return unique;
17
+ }
18
+ function getValueAtPath(data, path) {
19
+ const segments = path.split('.');
20
+ let current = data;
21
+ for (const segment of segments){
22
+ if (current == null || typeof current !== 'object') {
23
+ return undefined;
24
+ }
25
+ current = current[segment];
26
+ }
27
+ return current;
28
+ }
29
+ function collectUniqueFields(fields, prefix, data, result) {
30
+ for (const field of fields){
31
+ if (field.type === 'row' || field.type === 'collapsible') {
32
+ collectUniqueFields(field.fields, prefix, data, result);
33
+ continue;
34
+ }
35
+ if (field.type === 'tabs') {
36
+ for (const tab of field.tabs){
37
+ const tabPrefix = 'name' in tab && tab.name ? `${prefix}${tab.name}.` : prefix;
38
+ collectUniqueFields(tab.fields, tabPrefix, data, result);
39
+ }
40
+ continue;
41
+ }
42
+ if (!('name' in field) || !field.name) {
43
+ continue;
44
+ }
45
+ const path = prefix + field.name;
46
+ if ('unique' in field && field.unique) {
47
+ const value = getValueAtPath(data, path);
48
+ if (value != null) {
49
+ result.push({
50
+ paths: [
51
+ path
52
+ ]
53
+ });
54
+ }
55
+ }
56
+ if (field.type === 'group' && 'fields' in field) {
57
+ collectUniqueFields(field.fields, `${path}.`, data, result);
58
+ }
59
+ }
60
+ }
61
+
62
+ //# sourceMappingURL=buildUniquePaths.js.map
@@ -20,7 +20,7 @@ import path from 'path';
20
20
  const runShPath = path.join(projectPath, 'run.sh');
21
21
  const zipPath = path.join(projectPath, 'lambda.zip');
22
22
  if (!await isDirectory(standalonePath)) {
23
- throw new Error('Standalone build not found at .next/standalone. ' + 'Ensure next.config.js has output: "standalone" and run build first.');
23
+ throw new Error('Standalone build not found at .next/standalone. ' + 'Ensure next.config has output: "standalone" and run build first.');
24
24
  }
25
25
  if (await isDirectory(staticPath)) {
26
26
  const destStatic = path.join(standalonePath, '.next', 'static');
@@ -1,6 +1,6 @@
1
- import { parseModule, Syntax } from 'esprima-next';
2
1
  import fs from 'fs/promises';
3
2
  import path from 'path';
3
+ import { IndentationText, Node, Project, SyntaxKind } from 'ts-morph';
4
4
  const RUN_SH_CONTENT = `#!/bin/bash -x
5
5
 
6
6
  [ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache
@@ -36,11 +36,11 @@ NODE_ENV=production exec node server.js
36
36
  await addLambdaBuildScript(projectPath, packageManager);
37
37
  }
38
38
  /**
39
- * Find the Next.js config file (next.config.js or next.config.mjs)
40
- *
41
- * @todo Support TypeScript config files in future
39
+ * Find the Next.js config file, checking TS extensions first
42
40
  */ async function findNextConfigPath(projectPath) {
43
41
  const configFiles = [
42
+ 'next.config.ts',
43
+ 'next.config.mts',
44
44
  'next.config.js',
45
45
  'next.config.mjs'
46
46
  ];
@@ -53,83 +53,85 @@ NODE_ENV=production exec node server.js
53
53
  // File doesn't exist, try next one
54
54
  }
55
55
  }
56
- throw new Error('Could not find next.config.js or next.config.mjs');
56
+ throw new Error('Could not find next.config.ts, next.config.mts, next.config.js, or next.config.mjs');
57
57
  }
58
58
  /**
59
- * Add required Next.js config properties using AST parsing
59
+ * Add required Next.js config properties using ts-morph AST parsing
60
60
  * - output: 'standalone' (for Lambda deployment)
61
61
  * - eslint: { ignoreDuringBuilds: true } (allow builds with lint errors)
62
62
  */ async function addNextConfigProperties(projectPath) {
63
63
  const nextConfigPath = await findNextConfigPath(projectPath);
64
- const content = await fs.readFile(nextConfigPath, 'utf-8');
65
- // Check which properties already exist
66
- const hasOutput = content.includes('output:');
67
- const hasEslint = content.includes('eslint:');
68
- // Early exit if both already configured
69
- if (hasOutput && hasEslint) {
70
- return;
71
- }
72
- const ast = parseModule(content, {
73
- loc: true
64
+ const project = new Project({
65
+ compilerOptions: {
66
+ allowJs: true
67
+ },
68
+ manipulationSettings: {
69
+ indentationText: IndentationText.TwoSpaces
70
+ },
71
+ skipAddingFilesFromTsConfig: true
74
72
  });
75
- // Find export default declaration
76
- const exportDefaultDeclaration = ast.body.find((p)=>p.type === Syntax.ExportDefaultDeclaration);
77
- if (!exportDefaultDeclaration?.declaration?.loc) {
78
- throw new Error(`Could not find export default declaration in ${path.basename(nextConfigPath)}`);
79
- }
80
- // Find the object expression to modify
81
- let targetObjectExpression;
82
- if (exportDefaultDeclaration.declaration.type === 'ObjectExpression') {
83
- // Direct export: export default { ... }
84
- targetObjectExpression = exportDefaultDeclaration.declaration;
85
- } else if (exportDefaultDeclaration.declaration.type === 'CallExpression') {
86
- // Wrapped export: export default withPayload(nextConfig, ...)
87
- const callExpr = exportDefaultDeclaration.declaration;
88
- const firstArg = callExpr.arguments?.[0];
89
- if (firstArg?.type === 'Identifier') {
90
- // Find the variable declaration for this identifier
91
- const configVarName = firstArg.name;
92
- const varDeclaration = ast.body.find((node)=>{
93
- if (node.type === Syntax.VariableDeclaration) {
94
- const varNode = node;
95
- return varNode.declarations?.some((decl)=>decl.id?.type === 'Identifier' && decl.id.name === configVarName && decl.init?.type === 'ObjectExpression');
96
- }
97
- return false;
98
- });
99
- if (varDeclaration) {
100
- const declarator = varDeclaration.declarations?.find((decl)=>decl.id?.type === 'Identifier' && decl.id.name === configVarName);
101
- if (declarator?.init?.type === 'ObjectExpression') {
102
- targetObjectExpression = declarator.init;
103
- }
104
- }
105
- }
106
- }
107
- if (!targetObjectExpression?.loc) {
73
+ const sourceFile = project.addSourceFileAtPath(nextConfigPath);
74
+ // Find the config object to modify
75
+ const configObject = findConfigObject(sourceFile);
76
+ if (!configObject) {
108
77
  throw new Error(`Could not find Next.js config object in ${path.basename(nextConfigPath)}. ` + `Expected either 'export default { ... }' or 'export default wrapper(configVar, ...)'`);
109
78
  }
110
- const { loc } = targetObjectExpression;
111
- const lines = content.split(/\r?\n/);
112
- // Build insertion string based on what's missing
113
- const parts = [];
79
+ // Check which properties already exist using AST (avoids false positives from comments)
80
+ const hasOutput = configObject.getProperty('output') !== undefined;
81
+ const hasEslint = configObject.getProperty('eslint') !== undefined;
82
+ if (hasOutput && hasEslint) {
83
+ return;
84
+ }
114
85
  if (!hasOutput) {
115
- parts.push("output: 'standalone',");
86
+ configObject.addPropertyAssignment({
87
+ name: 'output',
88
+ initializer: "'standalone'"
89
+ });
116
90
  }
117
91
  if (!hasEslint) {
118
- parts.push(`eslint: {
119
- ignoreDuringBuilds: true,
120
- },`);
92
+ configObject.addPropertyAssignment({
93
+ name: 'eslint',
94
+ initializer: `{ ignoreDuringBuilds: true }`
95
+ });
96
+ }
97
+ await sourceFile.save();
98
+ }
99
+ /**
100
+ * Find the config object literal from either:
101
+ * - Direct export: `export default { ... }`
102
+ * - Wrapped export: `export default withPayload(configVar, ...)`
103
+ * where configVar is a variable declaration with an object literal
104
+ */ function findConfigObject(sourceFile) {
105
+ const exportDefault = sourceFile.getFirstDescendantByKind(SyntaxKind.ExportAssignment);
106
+ if (!exportDefault) {
107
+ return undefined;
108
+ }
109
+ const expression = exportDefault.getExpression();
110
+ // Direct export: export default { ... }
111
+ if (Node.isObjectLiteralExpression(expression)) {
112
+ return expression;
121
113
  }
122
- const insertString = '\n ' + parts.join('\n ');
123
- // Insert after opening brace
124
- const insertLine = loc.start.line - 1;
125
- const insertColumn = loc.start.column + 1;
126
- const targetLine = lines[insertLine];
127
- if (!targetLine) {
128
- throw new Error(`Could not find target line in ${path.basename(nextConfigPath)}`);
114
+ // Wrapped export: export default withPayload(configVar, ...)
115
+ if (Node.isCallExpression(expression)) {
116
+ const firstArg = expression.getArguments()[0];
117
+ // Variable reference: export default withPayload(configVar)
118
+ if (firstArg && Node.isIdentifier(firstArg)) {
119
+ const configVarName = firstArg.getText();
120
+ // Find the variable declaration
121
+ const varStatements = sourceFile.getVariableStatements();
122
+ for (const statement of varStatements){
123
+ for (const decl of statement.getDeclarations()){
124
+ if (decl.getName() === configVarName) {
125
+ const initializer = decl.getInitializer();
126
+ if (initializer && Node.isObjectLiteralExpression(initializer)) {
127
+ return initializer;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
129
133
  }
130
- lines[insertLine] = targetLine.slice(0, insertColumn) + insertString + targetLine.slice(insertColumn);
131
- const modifiedContent = lines.join('\n');
132
- await fs.writeFile(nextConfigPath, modifiedContent, 'utf-8');
134
+ return undefined;
133
135
  }
134
136
  /**
135
137
  * Add lambda:buildzip script to package.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/figma",
3
- "version": "0.0.1-alpha.56",
3
+ "version": "0.0.1-alpha.57",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,7 +34,6 @@
34
34
  "arg": "^5.0.2",
35
35
  "conf": "^13.0.1",
36
36
  "cross-spawn": "7.0.6",
37
- "esprima-next": "^6.0.2",
38
37
  "figures": "^6.1.0",
39
38
  "jose": "6.0.12",
40
39
  "jsonwebtoken": "9.0.3",